Faderport8 control surface support
authorRobin Gareus <robin@gareus.org>
Wed, 5 Apr 2017 09:04:16 +0000 (11:04 +0200)
committerRobin Gareus <robin@gareus.org>
Thu, 13 Apr 2017 19:21:59 +0000 (21:21 +0200)
19 files changed:
gtk2_ardour/ardev_common.sh.in
libs/ardour/ardour/debug.h
libs/ardour/debug.cc
libs/ardour/port_manager.cc
libs/surfaces/faderport8/actions.cc [new file with mode: 0644]
libs/surfaces/faderport8/callbacks.cc [new file with mode: 0644]
libs/surfaces/faderport8/faderport8.cc [new file with mode: 0644]
libs/surfaces/faderport8/faderport8.h [new file with mode: 0644]
libs/surfaces/faderport8/faderport8_interface.cc [new file with mode: 0644]
libs/surfaces/faderport8/fp8_base.h [new file with mode: 0644]
libs/surfaces/faderport8/fp8_button.h [new file with mode: 0644]
libs/surfaces/faderport8/fp8_controls.cc [new file with mode: 0644]
libs/surfaces/faderport8/fp8_controls.h [new file with mode: 0644]
libs/surfaces/faderport8/fp8_strip.cc [new file with mode: 0644]
libs/surfaces/faderport8/fp8_strip.h [new file with mode: 0644]
libs/surfaces/faderport8/gui.cc [new file with mode: 0644]
libs/surfaces/faderport8/gui.h [new file with mode: 0644]
libs/surfaces/faderport8/wscript [new file with mode: 0644]
libs/surfaces/wscript

index b785e36a10e3185d490261a85c407fa078507127..06dc9d0973a26b1a723557bc174624ebf47aeb11 100644 (file)
@@ -13,7 +13,7 @@ export GTK2_RC_FILES=/nonexistent
 # can find all the components.
 #
 
-export ARDOUR_SURFACES_PATH=$libs/surfaces/osc:$libs/surfaces/faderport:$libs/surfaces/generic_midi:$libs/surfaces/tranzport:$libs/surfaces/powermate:$libs/surfaces/mackie:$libs/surfaces/wiimote:$libs/surfaces/push2
+export ARDOUR_SURFACES_PATH=$libs/surfaces/osc:$libs/surfaces/faderport8:$libs/surfaces/faderport:$libs/surfaces/generic_midi:$libs/surfaces/tranzport:$libs/surfaces/powermate:$libs/surfaces/mackie:$libs/surfaces/wiimote:$libs/surfaces/push2
 export ARDOUR_PANNER_PATH=$libs/panners
 export ARDOUR_DATA_PATH=$TOP:$TOP/build:$TOP/gtk2_ardour:$TOP/build/gtk2_ardour:.
 export ARDOUR_MIDIMAPS_PATH=$TOP/midi_maps:.
index 0b7e5fefb5be80699545ab5b6360f82b93b634f8..eff8305bf158e6ad90057a559b2d7413e34994e8 100644 (file)
@@ -80,6 +80,7 @@ namespace PBD {
                LIBARDOUR_API extern DebugBits BackendPorts;
                LIBARDOUR_API extern DebugBits VSTCallbacks;
                LIBARDOUR_API extern DebugBits FaderPort;
+               LIBARDOUR_API extern DebugBits FaderPort8;
                LIBARDOUR_API extern DebugBits CC121;
                LIBARDOUR_API extern DebugBits VCA;
                LIBARDOUR_API extern DebugBits Push2;
index 8c88dd4d1d34a56194df6d123e3598ae8e29517e..5c645a80959da749b6320f636f66b6570262b738 100644 (file)
@@ -77,6 +77,7 @@ PBD::DebugBits PBD::DEBUG::BackendThreads = PBD::new_debug_bit ("backendthreads"
 PBD::DebugBits PBD::DEBUG::BackendPorts = PBD::new_debug_bit ("backendports");
 PBD::DebugBits PBD::DEBUG::VSTCallbacks = PBD::new_debug_bit ("vstcallbacks");
 PBD::DebugBits PBD::DEBUG::FaderPort = PBD::new_debug_bit ("faderport");
+PBD::DebugBits PBD::DEBUG::FaderPort8 = PBD::new_debug_bit ("faderport8");
 PBD::DebugBits PBD::DEBUG::CC121 = PBD::new_debug_bit ("cc121");
 PBD::DebugBits PBD::DEBUG::VCA = PBD::new_debug_bit ("vca");
 PBD::DebugBits PBD::DEBUG::Push2 = PBD::new_debug_bit ("push2");
index b99337a891dc831decadd74a3cb1bfb771f9f479..aa56ab8d438597a2b90baff1e5cce591a1ed48f9 100644 (file)
@@ -896,6 +896,7 @@ PortManager::port_is_control_only (std::string const& name)
                const char * const control_only_ports[] = {
                        X_(".*Ableton Push.*"),
                        X_(".*FaderPort .*"),
+                       X_(".*FaderPort8 .*"),
                };
 
                pattern = "(";
diff --git a/libs/surfaces/faderport8/actions.cc b/libs/surfaces/faderport8/actions.cc
new file mode 100644 (file)
index 0000000..949d19a
--- /dev/null
@@ -0,0 +1,470 @@
+/* Faderport 8 Control Surface
+ * This is the button "Controller" of the MVC surface inteface,
+ * see callbacks.cc for the "View".
+ *
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "ardour/dB.h"
+#include "ardour/session.h"
+#include "ardour/session_configuration.h"
+#include "ardour/types.h"
+
+#include "gtkmm2ext/actions.h"
+
+#include "faderport8.h"
+
+#include "pbd/i18n.h"
+
+using namespace ARDOUR;
+using namespace ArdourSurface;
+using namespace std;
+using namespace ArdourSurface::FP8Types;
+
+#define BindMethod(ID, CB) \
+       _ctrls.button (FP8Controls::ID).released.connect_same_thread (button_connections, boost::bind (&FaderPort8:: CB, this));
+
+#define BindFunction(ID, ACT, CB, ...) \
+       _ctrls.button (FP8Controls::ID). ACT .connect_same_thread (button_connections, boost::bind (&FaderPort8:: CB, this, __VA_ARGS__));
+
+#define BindAction(ID, GRP, ITEM) \
+       _ctrls.button (FP8Controls::ID).released.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_action, this, GRP, ITEM));
+
+#define BindUserAction(ID) \
+       _ctrls.button (ID).pressed.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_user, this, true, ID)); \
+_ctrls.button (ID).released.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_user, this, false, ID));
+
+void
+FaderPort8::setup_actions ()
+{
+       BindMethod (BtnPlay, button_play);
+       BindMethod (BtnStop, button_stop);
+       BindMethod (BtnLoop, button_loop);
+       BindMethod (BtnRecord, button_record);
+       BindMethod (BtnClick, button_metronom);
+       BindAction (BtnRedo, "Editor", "redo");
+
+       BindAction (BtnSave, "Common", "Save");
+       BindAction (BtnUndo, "Editor", "undo");
+       BindAction (BtnRedo, "Editor", "redo");
+
+       BindAction (BtnSoloClear, "Main", "cancel-solo");
+       BindMethod (BtnMuteClear, button_mute_clear);
+
+       BindMethod (FP8Controls::BtnArmAll, button_arm_all);
+
+       BindFunction (BtnRewind, pressed, button_varispeed, false);
+       BindFunction (BtnFastForward, pressed, button_varispeed, true);
+
+       BindFunction (BtnPrev, released, button_prev_next, false);
+       BindFunction (BtnNext, released, button_prev_next, true);
+
+       BindFunction (BtnArm, pressed, button_arm, true);
+       BindFunction (BtnArm, released, button_arm, false);
+
+       BindFunction (BtnAOff, released, button_automation, ARDOUR::Off);
+       BindFunction (BtnATouch, released, button_automation, ARDOUR::Touch);
+       BindFunction (BtnARead, released, button_automation, ARDOUR::Play);
+       BindFunction (BtnAWrite, released, button_automation, ARDOUR::Write);
+
+       _ctrls.button (FP8Controls::BtnEncoder).pressed.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_encoder, this));
+       _ctrls.button (FP8Controls::BtnParam).pressed.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_parameter, this));
+
+
+       BindAction (BtnBypass, "Mixer", "ab-plugins");
+       BindAction (BtnBypassAll, "Mixer", "ab-plugins"); // XXX
+
+       BindAction (BtnMacro, "Mixer", "show-editor");
+       BindAction (BtnLink, "Window", "show-mixer");
+
+       BindAction (BtnOpen, "Common", "addExistingAudioFiles");
+       BindAction (BtnLock, "Editor", "lock");
+
+       // user-specific
+       for (FP8Controls::UserButtonMap::const_iterator i = _ctrls.user_buttons ().begin ();
+                       i != _ctrls.user_buttons ().end (); ++i) {
+               BindUserAction ((*i).first);
+       }
+}
+
+void
+FaderPort8::button_play ()
+{
+       if (session->transport_rolling ()) {
+               if (session->transport_speed () != 1.0) {
+                       session->request_transport_speed (1.0);
+               } else {
+                       transport_stop ();
+               }
+       } else {
+               transport_play ();
+       }
+}
+
+void
+FaderPort8::button_stop ()
+{
+       transport_stop ();
+}
+
+void
+FaderPort8::button_record ()
+{
+       set_record_enable (!get_record_enabled ());
+}
+
+void
+FaderPort8::button_loop ()
+{
+       loop_toggle ();
+}
+
+void
+FaderPort8::button_metronom ()
+{
+       Config->set_clicking (!Config->get_clicking ());
+}
+
+void
+FaderPort8::button_automation (ARDOUR::AutoState as)
+{
+       FaderMode fadermode = _ctrls.fader_mode ();
+       switch (fadermode) {
+               case ModePlugins:
+#if 0 // Plugin Control Automation Mode
+                       for ( std::list <ProcessorCtrl>::iterator i = _proc_params.begin(); i != _proc_params.end(); ++i) {
+                               ((*i).ac)->set_automation_state (as);
+                       }
+#endif
+                       return;
+               case ModeSend:
+                       if (first_selected_stripable()) {
+#if 0 // Send Level Automation
+                               boost::shared_ptr<Stripable> s = first_selected_stripable();
+                               boost::shared_ptr<AutomationControl> send;
+                               uint32_t i = 0;
+                               while (0 != (send = s->send_level_controllable (i))) {
+                                       send->set_automation_state (as);
+                                       ++i;
+                               }
+#endif
+                       }
+                       return;
+               default:
+                       break;
+       }
+
+       // apply to all selected tracks
+       StripableList all;
+       session->get_stripables (all);
+       for (StripableList::const_iterator i = all.begin(); i != all.end(); ++i) {
+               if ((*i)->is_master() || (*i)->is_monitor()) {
+                       continue;
+               }
+               if (!(*i)->is_selected()) {
+                       continue;
+               }
+               boost::shared_ptr<AutomationControl> ac;
+               switch (fadermode) {
+                       case ModeTrack:
+                               ac = (*i)->gain_control ();
+                               break;
+                       case ModePan:
+                               ac = (*i)->pan_azimuth_control ();
+                               break;
+                       default:
+                               break;
+               }
+               if (ac) {
+                       ac->set_automation_state (as);
+               }
+       }
+}
+
+void
+FaderPort8::button_varispeed (bool ffw)
+{
+       /* pressing both rew + ffwd -> return to zero */
+       FP8ButtonInterface& b_rew = _ctrls.button (FP8Controls::BtnRewind);
+       FP8ButtonInterface& b_ffw = _ctrls.button (FP8Controls::BtnFastForward);
+       if (b_rew.is_pressed () && b_ffw.is_pressed ()){
+               // stop key-repeat
+               dynamic_cast<FP8RepeatButton*>(&b_ffw)->stop_repeat();
+               dynamic_cast<FP8RepeatButton*>(&b_rew)->stop_repeat();
+               AccessAction ("Transport", "GotoStart");
+               return;
+       }
+
+       // switch play direction, if needed
+       if (ffw) {
+               if (session->transport_speed () <= 0) {
+                       session->request_transport_speed (1.0);
+                       return ;
+               }
+       } else {
+               if (session->transport_speed () >= 0) {
+                       session->request_transport_speed (-1.0);
+                       return ;
+               }
+       }
+       // incremetally increase speed. double speed every 10 clicks
+       // (keypress auto-repeat is 100ms)
+       float maxspeed = Config->get_shuttle_max_speed();
+       float speed = exp2f(0.1f) * session->transport_speed ();
+       speed = std::max (-maxspeed, std::min (maxspeed, speed));
+       session->request_transport_speed (speed, false);
+}
+
+void
+FaderPort8::button_mute_clear ()
+{
+       StripableList all;
+       session->get_stripables (all);
+       boost::shared_ptr<ControlList> cl (new ControlList);
+       for (StripableList::const_iterator i = all.begin(); i != all.end(); ++i) {
+               if ((*i)->is_master() || (*i)->is_monitor()) {
+                       continue;
+               }
+               boost::shared_ptr<AutomationControl> ac = (*i)->mute_control();
+               if (ac) {
+                       cl->push_back (ac);
+               }
+       }
+       session->set_controls (cl, 0.0, PBD::Controllable::UseGroup);
+}
+
+void
+FaderPort8::button_arm (bool press)
+{
+       FaderMode fadermode = _ctrls.fader_mode ();
+       if (fadermode == ModeTrack || fadermode == ModePan) {
+               _ctrls.button (FP8Controls::BtnArm).set_active (press);
+               ARMButtonChange (press);
+       }
+}
+
+void
+FaderPort8::button_arm_all ()
+{
+       BasicUI::all_tracks_rec_in ();
+}
+
+void
+FaderPort8::button_action (const std::string& group, const std::string& item)
+{
+       AccessAction (group, item);
+}
+
+void
+FaderPort8::button_prev_next (bool next)
+{
+       switch (_ctrls.nav_mode()) {
+               case NavMaster:
+               case NavChannel:
+               case NavScroll:
+                       bank (!next, false);
+                       break;
+               case NavBank:
+                       bank (!next, true);
+                       break;
+               case NavZoom:
+                       if (next) {
+                               StepTracksDown ();
+                       } else {
+                               StepTracksUp ();
+                       }
+                       break;
+               case NavSection:
+                       // TODO nudge
+                       break;
+               case NavMarker:
+                       if (next) {
+                               next_marker ();
+                       } else {
+                               prev_marker ();
+                       }
+                       break;
+       }
+}
+
+/* handle navigation encoder press */
+void
+FaderPort8::button_encoder ()
+{
+       switch (_ctrls.nav_mode()) {
+               case NavZoom:
+                       ZoomToSession (); // XXX undo zoom
+                       break;
+               case NavScroll:
+                       ZoomToSession ();
+                       break;
+               case NavChannel:
+               case NavBank:
+                       move_selected_into_view ();
+                       break;
+               case NavMaster:
+                       {
+                               /* master || monitor level -- reset to 0dB */
+                               boost::shared_ptr<AutomationControl> ac;
+                               if (session->monitor_active() && !_ctrls.button (FP8Controls::BtnMaster).is_pressed ()) {
+                                       ac = session->monitor_out()->gain_control ();
+                               } else if (session->master_out()) {
+                                       ac = session->master_out()->gain_control ();
+                               }
+                               if (ac) {
+                                       ac->set_value (ac->normal(), PBD::Controllable::NoGroup);
+                               }
+                       }
+                       break;
+               case NavSection:
+                       break;
+               case NavMarker:
+                       {
+                               string markername;
+                               /* Don't add another mark if one exists within 1/100th of a second of
+                                * the current position and we're not rolling.
+                                */
+                               framepos_t where = session->audible_frame();
+                               if (session->transport_stopped() && session->locations()->mark_at (where, session->frame_rate() / 100.0)) {
+                                       return;
+                               }
+
+                               session->locations()->next_available_name (markername,"mark");
+                               add_marker (markername);
+                       }
+                       break;
+       }
+}
+
+/* handle navigation encoder turn */
+void
+FaderPort8::encoder_navigate (bool neg, int steps)
+{
+       /* special-case metronome level */
+       if (_ctrls.button (FP8Controls::BtnClick).is_pressed ()) {
+               // compare to ARDOUR_UI::click_button_scroll()
+               gain_t gain = Config->get_click_gain();
+               float gain_db = accurate_coefficient_to_dB (gain);
+               gain_db += (neg ? -1.f : 1.f) * steps;
+               gain_db = std::max (-60.f, gain_db);
+               gain = dB_to_coefficient (gain_db);
+               gain = std::min (gain, Config->get_max_gain());
+               Config->set_click_gain (gain);
+               _ctrls.button (FP8Controls::BtnClick).ignore_release();
+               return;
+       }
+
+       switch (_ctrls.nav_mode()) {
+               case NavChannel:
+                       if (neg) {
+                               StepTracksUp ();
+                       } else {
+                               StepTracksDown ();
+                       }
+                       break;
+               case NavZoom:
+                       if (neg) {
+                               ZoomOut ();
+                       } else {
+                               ZoomIn ();
+                       }
+                       break;
+               case NavMarker:
+               case NavScroll:
+                       ScrollTimeline ((neg ? -1.f : 1.f) * steps / (shift_mod() ? 1024.f : 256.f));
+                       break;
+               case NavBank:
+                       bank (neg, false);
+                       break;
+               case NavMaster:
+                       {
+                               /* master || monitor level */
+                               boost::shared_ptr<AutomationControl> ac;
+                               if (session->monitor_active() && !_ctrls.button (FP8Controls::BtnMaster).is_pressed ()) {
+                                       ac = session->monitor_out()->gain_control ();
+                               } else if (session->master_out()) {
+                                       ac = session->master_out()->gain_control ();
+                               }
+                               if (ac) {
+                                       double v = ac->internal_to_interface (ac->get_value());
+                                       v = std::max (0.0, std::min (1.0, v + steps * (neg ? -.01 : .01)));
+                                       ac->set_value (ac->interface_to_internal(v), PBD::Controllable::NoGroup);
+                               }
+                       }
+                       break;
+               case NavSection:
+                       // nudge event
+                       break;
+       }
+}
+
+/* handle pan/param encoder press */
+void
+FaderPort8::button_parameter ()
+{
+       switch (_ctrls.fader_mode()) {
+               case ModeTrack:
+               case ModePan:
+                       // pan-width see FaderPort8::encoder_parameter()
+                       break;
+               case ModePlugins:
+                       break;
+               case ModeSend:
+                       break;
+       }
+}
+
+/* handle pan/param encoder turn */
+void
+FaderPort8::encoder_parameter (bool neg, int steps)
+{
+       switch (_ctrls.fader_mode()) {
+               case ModeTrack:
+               case ModePan:
+                       {
+                               boost::shared_ptr<Stripable> s = first_selected_stripable();
+                               if (s) {
+                                       boost::shared_ptr<AutomationControl> ac;
+                                       if (_ctrls.button (FP8Controls::BtnParam).is_pressed ()) {
+                                               ac = s->pan_width_control ();
+                                       } else {
+                                               ac = s->pan_azimuth_control ();
+                                       }
+                                       if (ac) {
+                                               double v = ac->internal_to_interface (ac->get_value());
+                                               v = std::max (0.0, std::min (1.0, v + steps * (neg ? -.01 : .01)));
+                                               ac->set_value (ac->interface_to_internal(v), PBD::Controllable::UseGroup);
+                                       }
+                               }
+                       }
+                       break;
+               case ModePlugins:
+               case ModeSend:
+                       while (steps > 0) {
+                               bank_param (neg, false);
+                               --steps;
+                       }
+                       break;
+       }
+}
+
+/* handle user-specific actions */
+void
+FaderPort8::button_user (bool press, FP8Controls::ButtonId btn)
+{
+       _user_action_map[btn].call (*this, press);
+}
diff --git a/libs/surfaces/faderport8/callbacks.cc b/libs/surfaces/faderport8/callbacks.cc
new file mode 100644 (file)
index 0000000..72aa822
--- /dev/null
@@ -0,0 +1,206 @@
+/* Faderport 8 Control Surface
+ * This is the button "View" of the MVC surface inteface,
+ * see actions.cc for the "Controller"
+ *
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "ardour/session.h"
+#include "ardour/session_configuration.h"
+
+#include "gtkmm2ext/actions.h"
+
+#include "faderport8.h"
+
+#include "pbd/i18n.h"
+
+using namespace ARDOUR;
+using namespace ArdourSurface;
+using namespace ArdourSurface::FP8Types;
+
+void
+FaderPort8::connect_session_signals ()
+{
+        session->RouteAdded.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_stripable_added_or_removed, this), this);
+        PresentationInfo::Change.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_pi_property_changed, this, _1), this);
+
+       Config->ParameterChanged.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_parameter_changed, this, _1), this);
+       session->config.ParameterChanged.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_parameter_changed, this, _1), this);
+
+       session->TransportStateChange.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_transport_state_changed, this), this);
+       session->TransportLooped.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_loop_state_changed, this), this);
+       session->RecordStateChanged.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_record_state_changed, this), this);
+
+       session->DirtyChanged.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_session_dirty_changed, this), this);
+       session->SoloChanged.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_solo_changed, this), this);
+       session->MuteChanged.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_mute_changed, this), this);
+       session->history().Changed.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_history_changed, this), this);
+}
+
+void
+FaderPort8::send_session_state ()
+{
+       notify_transport_state_changed ();
+       notify_record_state_changed ();
+       notify_session_dirty_changed ();
+       notify_history_changed ();
+       notify_solo_changed ();
+       notify_mute_changed ();
+       notify_parameter_changed ("clicking");
+
+       notify_automation_mode_changed (); // XXX (stip specific, see below)
+}
+
+// TODO: AutomationState display of plugin & send automation ?!
+void
+FaderPort8::notify_automation_mode_changed ()
+{
+       boost::shared_ptr<Stripable> s = first_selected_stripable();
+       boost::shared_ptr<AutomationControl> ac;
+       if (s) {
+               switch (_ctrls.fader_mode ()) {
+                       case ModeTrack:
+                               ac = s->gain_control();
+                               break;
+                       case ModePan:
+                               ac = s->pan_azimuth_control();
+                               break;
+                       default:
+                               break;
+               }
+       }
+       if (!s || !ac) {
+               _ctrls.button (FP8Controls::BtnALatch).set_active (false);
+               _ctrls.button (FP8Controls::BtnATrim).set_active (false);
+               _ctrls.button (FP8Controls::BtnAOff).set_active (false);
+               _ctrls.button (FP8Controls::BtnATouch).set_active (false);
+               _ctrls.button (FP8Controls::BtnARead).set_active (false);
+               _ctrls.button (FP8Controls::BtnAWrite).set_active (false);
+               return;
+       }
+
+       ARDOUR::AutoState as = ac->automation_state();
+       _ctrls.button (FP8Controls::BtnAOff).set_active (as == Off);
+       _ctrls.button (FP8Controls::BtnATouch).set_active (as == Touch);
+       _ctrls.button (FP8Controls::BtnARead).set_active (as == Play);
+       _ctrls.button (FP8Controls::BtnAWrite).set_active (as == Write);
+}
+
+void
+FaderPort8::notify_parameter_changed (std::string param)
+{
+       if (param == "clicking") {
+               _ctrls.button (FP8Controls::BtnClick).set_active (Config->get_clicking ());
+       }
+}
+
+void
+FaderPort8::notify_transport_state_changed ()
+{
+       if (session->transport_rolling ()) {
+               _ctrls.button (FP8Controls::BtnPlay).set_active (true);
+               _ctrls.button (FP8Controls::BtnStop).set_active (false);
+       } else {
+               _ctrls.button (FP8Controls::BtnPlay).set_active (false);
+               _ctrls.button (FP8Controls::BtnStop).set_active (true);
+       }
+
+       /* set rewind/fastforward lights */
+       const float ts = session->transport_speed ();
+       FP8ButtonInterface& b_rew = _ctrls.button (FP8Controls::BtnRewind);
+       FP8ButtonInterface& b_ffw = _ctrls.button (FP8Controls::BtnFastForward);
+
+       const bool rew = (ts < 0.f);
+       const bool ffw = (ts > 0.f && ts != 1.f);
+       if (b_rew.is_active() != rew) {
+               b_rew.set_active (rew);
+       }
+       if (b_ffw.is_active() != ffw) {
+               b_ffw.set_active (ffw);
+       }
+
+       notify_loop_state_changed ();
+}
+
+void
+FaderPort8::notify_record_state_changed ()
+{
+       switch (session->record_status ()) {
+               case Session::Disabled:
+                       _ctrls.button (FP8Controls::BtnRecord).set_active (0);
+                       _ctrls.button (FP8Controls::BtnRecord).set_blinking (false);
+                       break;
+               case Session::Enabled:
+                       _ctrls.button (FP8Controls::BtnRecord).set_active (true);
+                       _ctrls.button (FP8Controls::BtnRecord).set_blinking (true);
+                       break;
+               case Session::Recording:
+                       _ctrls.button (FP8Controls::BtnRecord).set_active (true);
+                       _ctrls.button (FP8Controls::BtnRecord).set_blinking (false);
+                       break;
+       }
+}
+
+void
+FaderPort8::notify_loop_state_changed ()
+{
+       bool looping = false;
+       Location* looploc = session->locations ()->auto_loop_location ();
+       if (looploc && session->get_play_loop ()) {
+               looping = true;
+       }
+       _ctrls.button (FP8Controls::BtnLoop).set_active (looping);
+}
+
+void
+FaderPort8::notify_session_dirty_changed ()
+{
+       const bool is_dirty = session->dirty ();
+       _ctrls.button (FP8Controls::BtnSave).set_active (is_dirty);
+       _ctrls.button (FP8Controls::BtnSave).set_color (is_dirty ? 0xff0000ff : 0x00ff00ff);
+}
+
+void
+FaderPort8::notify_history_changed ()
+{
+       _ctrls.button (FP8Controls::BtnRedo).set_active (session->redo_depth() > 0);
+       _ctrls.button (FP8Controls::BtnUndo).set_active (session->undo_depth() > 0);
+}
+
+void
+FaderPort8::notify_solo_changed ()
+{
+       _ctrls.button (FP8Controls::BtnSoloClear).set_active (session->soloing() || session->listening());
+}
+
+void
+FaderPort8::notify_mute_changed ()
+{
+       bool muted = false;
+       boost::shared_ptr<RouteList> rl = session->get_routes();
+       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
+               if ((*i)->is_master() || (*i)->is_monitor()) {
+                       continue;
+               }
+               boost::shared_ptr<MuteControl> mc = (*i)->mute_control();
+               if (mc && mc->muted ()) {
+                       muted = true;
+                       break;
+               }
+       }
+       _ctrls.button (FP8Controls::BtnMuteClear).set_active (muted);
+}
diff --git a/libs/surfaces/faderport8/faderport8.cc b/libs/surfaces/faderport8/faderport8.cc
new file mode 100644 (file)
index 0000000..424742a
--- /dev/null
@@ -0,0 +1,1501 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ * Copyright (C) 2015 Paul Davis
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include <cstdlib>
+#include <sstream>
+#include <algorithm>
+
+#include <stdint.h>
+
+#include "pbd/error.h"
+#include "pbd/failed_constructor.h"
+#include "pbd/pthread_utils.h"
+#include "pbd/compose.h"
+#include "pbd/xml++.h"
+
+#include "midi++/port.h"
+
+#include "ardour/audioengine.h"
+#include "ardour/audio_track.h"
+#include "ardour/bundle.h"
+#include "ardour/debug.h"
+#include "ardour/midi_track.h"
+#include "ardour/midiport_manager.h"
+#include "ardour/plugin_insert.h"
+#include "ardour/processor.h"
+#include "ardour/rc_configuration.h"
+#include "ardour/route.h"
+#include "ardour/session.h"
+#include "ardour/session_configuration.h"
+#include "ardour/vca.h"
+
+#include "faderport8.h"
+
+using namespace ARDOUR;
+using namespace ArdourSurface;
+using namespace PBD;
+using namespace Glib;
+using namespace std;
+using namespace ArdourSurface::FP8Types;
+
+#include "pbd/i18n.h"
+
+#include "pbd/abstract_ui.cc" // instantiate template
+
+#ifndef NDEBUG
+//#define VERBOSE_DEBUG
+#endif
+
+static void
+debug_2byte_msg (std::string const& msg, int b0, int b1)
+{
+#ifndef NDEBUG
+       if (DEBUG_ENABLED(DEBUG::FaderPort8)) {
+               DEBUG_STR_DECL(a);
+               DEBUG_STR_APPEND(a, "RECV: ");
+               DEBUG_STR_APPEND(a, msg);
+               DEBUG_STR_APPEND(a,' ');
+               DEBUG_STR_APPEND(a,hex);
+               DEBUG_STR_APPEND(a,"0x");
+               DEBUG_STR_APPEND(a, b0);
+               DEBUG_STR_APPEND(a,' ');
+               DEBUG_STR_APPEND(a,"0x");
+               DEBUG_STR_APPEND(a, b1);
+               DEBUG_STR_APPEND(a,'\n');
+               DEBUG_TRACE (DEBUG::FaderPort8, DEBUG_STR(a).str());
+       }
+#endif
+}
+
+FaderPort8::FaderPort8 (Session& s)
+       : ControlProtocol (s, _("PreSonus FaderPort8"))
+       , AbstractUI<FaderPort8Request> (name())
+       , _connection_state (ConnectionState (0))
+       , _device_active (false)
+       , _ctrls (*this)
+       , _channel_off (0)
+       , _plugin_off (0)
+       , _parameter_off (0)
+       , _blink_onoff (false)
+       , _shift_lock (false)
+       , _shift_pressed (false)
+       , gui (0)
+{
+       boost::shared_ptr<ARDOUR::Port> inp;
+       boost::shared_ptr<ARDOUR::Port> outp;
+
+       inp  = AudioEngine::instance()->register_input_port (DataType::MIDI, "FaderPort8 Recv", true);
+       outp = AudioEngine::instance()->register_output_port (DataType::MIDI, "FaderPort8 Send", true);
+       _input_port = boost::dynamic_pointer_cast<AsyncMIDIPort>(inp);
+       _output_port = boost::dynamic_pointer_cast<AsyncMIDIPort>(outp);
+
+       if (_input_port == 0 || _output_port == 0) {
+               throw failed_constructor();
+       }
+
+       _input_bundle.reset (new ARDOUR::Bundle (_("FaderPort8 (Receive)"), true));
+       _output_bundle.reset (new ARDOUR::Bundle (_("FaderPort8 (Send) "), false));
+
+       _input_bundle->add_channel (
+               inp->name(),
+               ARDOUR::DataType::MIDI,
+               session->engine().make_port_name_non_relative (inp->name())
+               );
+
+       _output_bundle->add_channel (
+               outp->name(),
+               ARDOUR::DataType::MIDI,
+               session->engine().make_port_name_non_relative (outp->name())
+               );
+
+       ARDOUR::AudioEngine::instance()->PortConnectedOrDisconnected.connect (port_connection, MISSING_INVALIDATOR, boost::bind (&FaderPort8::connection_handler, this, _1, _2, _3, _4, _5), this);
+
+       StripableSelectionChanged.connect (selection_connection, MISSING_INVALIDATOR, boost::bind (&FaderPort8::gui_track_selection_changed, this), this);
+
+       setup_actions ();
+       _ctrls.FaderModeChanged.connect_same_thread (modechange_connections, boost::bind (&FaderPort8::notify_fader_mode_changed, this));
+       _ctrls.MixModeChanged.connect_same_thread (modechange_connections, boost::bind (&FaderPort8::assign_strips, this, true));
+}
+
+FaderPort8::~FaderPort8 ()
+{
+       cerr << "~FP8\n";
+       stop_midi_handling  ();
+       close ();
+
+       if (_input_port) {
+               DEBUG_TRACE (DEBUG::FaderPort8, string_compose ("unregistering input port %1\n", boost::shared_ptr<ARDOUR::Port>(_input_port)->name()));
+               AudioEngine::instance()->unregister_port (_input_port);
+               _input_port.reset ();
+       }
+
+       if (_output_port) {
+               _output_port->drain (10000,  250000); /* check every 10 msecs, wait up to 1/4 second for the port to drain */
+               DEBUG_TRACE (DEBUG::FaderPort8, string_compose ("unregistering output port %1\n", boost::shared_ptr<ARDOUR::Port>(_output_port)->name()));
+               AudioEngine::instance()->unregister_port (_output_port);
+               _output_port.reset ();
+       }
+
+       tear_down_gui ();
+
+       /* stop event loop */
+       DEBUG_TRACE (DEBUG::FaderPort8, "BaseUI::quit ()\n");
+       BaseUI::quit ();
+}
+
+/* ****************************************************************************
+ * Event Loop
+ */
+
+void*
+FaderPort8::request_factory (uint32_t num_requests)
+{
+       /* AbstractUI<T>::request_buffer_factory() is a template method only
+        * instantiated in this source module. To provide something visible for
+        * use in the interface/descriptor, we have this static method that is
+        * template-free.
+        */
+       return request_buffer_factory (num_requests);
+}
+
+void
+FaderPort8::do_request (FaderPort8Request* req)
+{
+       if (req->type == CallSlot) {
+               call_slot (MISSING_INVALIDATOR, req->the_slot);
+       } else if (req->type == Quit) {
+               stop ();
+       }
+}
+
+int
+FaderPort8::stop ()
+{
+       BaseUI::quit ();
+       return 0;
+}
+
+void
+FaderPort8::thread_init ()
+{
+       struct sched_param rtparam;
+
+       pthread_set_name (event_loop_name().c_str());
+
+       PBD::notify_event_loops_about_thread_creation (pthread_self(), event_loop_name(), 2048);
+       ARDOUR::SessionEvent::create_per_thread_pool (event_loop_name(), 128);
+
+       memset (&rtparam, 0, sizeof (rtparam));
+       rtparam.sched_priority = 9; /* XXX should be relative to audio (JACK) thread */
+
+       if (pthread_setschedparam (pthread_self(), SCHED_FIFO, &rtparam) != 0) {
+               // do we care? not particularly.
+       }
+}
+
+bool
+FaderPort8::periodic ()
+{
+       /* prepare TC display -- handled by stripable Periodic () */
+       if (_ctrls.display_timecode ()) {
+               // TODO allow BBT, HHMMSS
+               // used in FP8Strip::periodic_update_timecode
+               Timecode::Time TC;
+               session->timecode_time (TC);
+               _timecode = Timecode::timecode_format_time(TC);
+       } else {
+               _timecode.clear ();
+       }
+
+       /* update stripables */
+       Periodic ();
+       return true;
+}
+
+bool
+FaderPort8::blink_it ()
+{
+       _blink_onoff = !_blink_onoff;
+       BlinkIt (_blink_onoff);
+       return true;
+}
+
+/* ****************************************************************************
+ * Port and Signal Connection Management
+ */
+int
+FaderPort8::set_active (bool yn)
+{
+       DEBUG_TRACE (DEBUG::FaderPort8, string_compose("set_active init with yn: '%1'\n", yn));
+
+       if (yn == active()) {
+               return 0;
+       }
+
+       if (yn) {
+               /* start event loop */
+               BaseUI::run ();
+               connect_session_signals ();
+       } else {
+               BaseUI::quit ();
+               close ();
+       }
+
+       ControlProtocol::set_active (yn);
+       DEBUG_TRACE (DEBUG::FaderPort8, string_compose("set_active done with yn: '%1'\n", yn));
+       return 0;
+}
+
+void
+FaderPort8::close ()
+{
+       _assigned_strips.clear ();
+       stop_midi_handling ();
+       session_connections.drop_connections ();
+       automation_state_connections.drop_connections ();
+       assigned_stripable_connections.drop_connections ();
+       drop_ctrl_connections ();
+       port_connection.disconnect ();
+       selection_connection.disconnect ();
+}
+
+void
+FaderPort8::stop_midi_handling ()
+{
+       _periodic_connection.disconnect ();
+       _blink_connection.disconnect ();
+       midi_connections.drop_connections ();
+       /* Note: the input handler is still active at this point, but we're no
+        * longer connected to any of the parser signals
+        */
+}
+
+void
+FaderPort8::connected ()
+{
+       DEBUG_TRACE (DEBUG::FaderPort8, "initializing\n");
+       // ideally check firmware version >= 1.01 (USB bcdDevice 0x0101) (vendor 0x194f prod 0x0202)
+       // but we don't have a handle to the underlying USB device here.
+
+       _channel_off = _plugin_off = _parameter_off = 0;
+       _blink_onoff = false;
+       _shift_lock = false;
+       _shift_pressed = false;
+
+       start_midi_handling ();
+       _ctrls.initialize ();
+
+       /* highlight bound user-actions */
+       for (FP8Controls::UserButtonMap::const_iterator i = _ctrls.user_buttons ().begin ();
+                       i != _ctrls.user_buttons ().end (); ++i) {
+               _ctrls.button (i->first).set_active (! _user_action_map[i->first].empty ());
+       }
+       /* shift button lights */
+       tx_midi3 (0x90, 0x06, 0x00);
+       tx_midi3 (0x90, 0x46, 0x00);
+
+       send_session_state ();
+       assign_strips (true);
+
+       Glib::RefPtr<Glib::TimeoutSource> blink_timer =
+               Glib::TimeoutSource::create (200);
+       _blink_connection = blink_timer->connect (sigc::mem_fun (*this, &FaderPort8::blink_it));
+       blink_timer->attach (main_loop()->get_context());
+
+       Glib::RefPtr<Glib::TimeoutSource> periodic_timer =
+               Glib::TimeoutSource::create (100);
+       _periodic_connection = periodic_timer->connect (sigc::mem_fun (*this, &FaderPort8::periodic));
+       periodic_timer->attach (main_loop()->get_context());
+}
+
+void
+FaderPort8::disconnected ()
+{
+       stop_midi_handling ();
+       for (uint8_t id = 0; id < 8; ++id) {
+               _ctrls.strip(id).unset_controllables ();
+       }
+}
+
+bool
+FaderPort8::connection_handler (boost::weak_ptr<ARDOUR::Port>, std::string name1, boost::weak_ptr<ARDOUR::Port>, std::string name2, bool yn)
+{
+#ifdef VERBOSE_DEBUG
+       DEBUG_TRACE (DEBUG::FaderPort8, "FaderPort8::connection_handler: start\n");
+#endif
+       if (!_input_port || !_output_port) {
+               return false;
+       }
+
+       string ni = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (boost::shared_ptr<ARDOUR::Port>(_input_port)->name());
+       string no = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (boost::shared_ptr<ARDOUR::Port>(_output_port)->name());
+
+       if (ni == name1 || ni == name2) {
+               if (yn) {
+                       _connection_state |= InputConnected;
+               } else {
+                       _connection_state &= ~InputConnected;
+               }
+       } else if (no == name1 || no == name2) {
+               if (yn) {
+                       _connection_state |= OutputConnected;
+               } else {
+                       _connection_state &= ~OutputConnected;
+               }
+       } else {
+#ifdef VERBOSE_DEBUG
+               DEBUG_TRACE (DEBUG::FaderPort8, string_compose ("Connections between %1 and %2 changed, but I ignored it\n", name1, name2));
+#endif
+               /* not our ports */
+               return false;
+       }
+
+       if ((_connection_state & (InputConnected|OutputConnected)) == (InputConnected|OutputConnected)) {
+
+               /* XXX this is a horrible hack. Without a short sleep here,
+                * something prevents the device wakeup messages from being
+                * sent and/or the responses from being received.
+                */
+               g_usleep (100000);
+               DEBUG_TRACE (DEBUG::FaderPort8, "device now connected for both input and output\n");
+               connected ();
+               _device_active = true;
+
+       } else {
+               DEBUG_TRACE (DEBUG::FaderPort8, "Device disconnected (input or output or both) or not yet fully connected\n");
+               disconnected ();
+               _device_active = false;
+       }
+
+       ConnectionChange (); /* emit signal for our GUI */
+
+#ifdef VERBOSE_DEBUG
+       DEBUG_TRACE (DEBUG::FaderPort8, "FaderPort8::connection_handler: end\n");
+#endif
+
+       return true; /* connection status changed */
+}
+
+list<boost::shared_ptr<ARDOUR::Bundle> >
+FaderPort8::bundles ()
+{
+       list<boost::shared_ptr<ARDOUR::Bundle> > b;
+
+       if (_input_bundle) {
+               b.push_back (_input_bundle);
+               b.push_back (_output_bundle);
+       }
+
+       return b;
+}
+
+/* ****************************************************************************
+ * MIDI I/O
+ */
+bool
+FaderPort8::midi_input_handler (Glib::IOCondition ioc, boost::weak_ptr<ARDOUR::AsyncMIDIPort> wport)
+{
+       boost::shared_ptr<AsyncMIDIPort> port (wport.lock());
+
+       if (!port) {
+               return false;
+       }
+
+#ifdef VERBOSE_DEBUG
+       DEBUG_TRACE (DEBUG::FaderPort8, string_compose ("something happend on %1\n", boost::shared_ptr<MIDI::Port>(port)->name()));
+#endif
+
+       if (ioc & ~IO_IN) {
+               return false;
+       }
+
+       if (ioc & IO_IN) {
+
+               port->clear ();
+#ifdef VERBOSE_DEBUG
+               DEBUG_TRACE (DEBUG::FaderPort8, string_compose ("data available on %1\n", boost::shared_ptr<MIDI::Port>(port)->name()));
+#endif
+               framepos_t now = session->engine().sample_time();
+               port->parse (now);
+       }
+
+       return true;
+}
+
+void
+FaderPort8::start_midi_handling ()
+{
+       _input_port->parser()->sysex.connect_same_thread (midi_connections, boost::bind (&FaderPort8::sysex_handler, this, _1, _2, _3));
+       _input_port->parser()->poly_pressure.connect_same_thread (midi_connections, boost::bind (&FaderPort8::polypressure_handler, this, _1, _2));
+       for (uint8_t i = 0; i < 16; ++i) {
+       _input_port->parser()->channel_pitchbend[i].connect_same_thread (midi_connections, boost::bind (&FaderPort8::pitchbend_handler, this, _1, i, _2));
+       }
+       _input_port->parser()->controller.connect_same_thread (midi_connections, boost::bind (&FaderPort8::controller_handler, this, _1, _2));
+       _input_port->parser()->note_on.connect_same_thread (midi_connections, boost::bind (&FaderPort8::note_on_handler, this, _1, _2));
+       _input_port->parser()->note_off.connect_same_thread (midi_connections, boost::bind (&FaderPort8::note_off_handler, this, _1, _2));
+
+       /* This connection means that whenever data is ready from the input
+        * port, the relevant thread will invoke our ::midi_input_handler()
+        * method, which will read the data, and invoke the parser.
+        */
+       _input_port->xthread().set_receive_handler (sigc::bind (sigc::mem_fun (this, &FaderPort8::midi_input_handler), boost::weak_ptr<AsyncMIDIPort> (_input_port)));
+       _input_port->xthread().attach (main_loop()->get_context());
+}
+
+size_t
+FaderPort8::tx_midi (std::vector<uint8_t> const& d) const
+{
+       /* work around midi buffer overflow for batch changes */
+       if (d.size() == 3 && (d[0] == 0x91 || d[0] == 0x92)) {
+               /* set colors triplet in one go */
+       } else if (d.size() == 3 && (d[0] == 0x93)) {
+               g_usleep (1500);
+       } else {
+               g_usleep (400 * d.size());
+       }
+#ifndef NDEBUG
+       size_t tx = _output_port->write (&d[0], d.size(), 0);
+       assert (tx == d.size());
+       return tx;
+#else
+       return _output_port->write (&d[0], d.size(), 0);
+#endif
+}
+
+/* ****************************************************************************
+ * MIDI Callbacks
+ */
+void
+FaderPort8::polypressure_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb)
+{
+       debug_2byte_msg ("PP", tb->controller_number, tb->value);
+       // outgoing only (meter)
+}
+
+void
+FaderPort8::pitchbend_handler (MIDI::Parser &, uint8_t chan, MIDI::pitchbend_t pb)
+{
+       debug_2byte_msg ("PB", chan, pb);
+       /* fader 0..16368 (0x3ff0 -- 1024 steps) */
+       _ctrls.midi_fader (chan, pb);
+}
+
+void
+FaderPort8::controller_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb)
+{
+       debug_2byte_msg ("CC", tb->controller_number, tb->value);
+       // encoder
+       // val Bit 7 = direction, Bits 0-6 = number of steps
+       if (tb->controller_number == 0x3c) {
+               encoder_navigate (tb->value & 0x40 ? true : false, tb->value & 0x3f);
+       }
+       if (tb->controller_number == 0x10) {
+               encoder_parameter (tb->value & 0x40 ? true : false, tb->value & 0x3f);
+       }
+}
+
+void
+FaderPort8::note_on_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb)
+{
+       debug_2byte_msg ("ON", tb->note_number, tb->velocity);
+
+       /* fader touch */
+       if (tb->note_number >= 0x68 && tb->note_number <= 0x6f) {
+               _ctrls.midi_touch (tb->note_number - 0x68, tb->velocity);
+               return;
+       }
+
+       /* special case shift */
+       if (tb->note_number == 0x06 || tb->note_number == 0x46) {
+               _shift_pressed = true;
+               _shift_connection.disconnect ();
+               if (_shift_lock) {
+                       _shift_lock = false;
+                       ShiftButtonChange (false);
+                       tx_midi3 (0x90, 0x06, 0x00);
+                       tx_midi3 (0x90, 0x46, 0x00);
+                       return;
+               }
+
+               Glib::RefPtr<Glib::TimeoutSource> shift_timer =
+                       Glib::TimeoutSource::create (1000);
+               shift_timer->attach (main_loop()->get_context());
+               _shift_connection = shift_timer->connect (sigc::mem_fun (*this, &FaderPort8::shift_timeout));
+
+               ShiftButtonChange (true);
+               tx_midi3 (0x90, 0x06, 0x7f);
+               tx_midi3 (0x90, 0x46, 0x7f);
+               return;
+       }
+
+       _ctrls.midi_event (tb->note_number, tb->velocity);
+}
+
+void
+FaderPort8::note_off_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb)
+{
+       debug_2byte_msg ("OF", tb->note_number, tb->velocity);
+
+       if (tb->note_number >= 0x68 && tb->note_number <= 0x6f) {
+               // fader touch
+               _ctrls.midi_touch (tb->note_number - 0x68, tb->velocity);
+               return;
+       }
+
+       /* special case shift */
+       if (tb->note_number == 0x06 || tb->note_number == 0x46) {
+               _shift_pressed = false;
+               if (_shift_lock) {
+                       return;
+               }
+               ShiftButtonChange (false);
+               tx_midi3 (0x90, 0x06, 0x00);
+               tx_midi3 (0x90, 0x46, 0x00);
+               /* just in case this happens concurrently */
+               _shift_connection.disconnect ();
+               _shift_lock = false;
+               return;
+       }
+
+       bool handled = _ctrls.midi_event (tb->note_number, tb->velocity);
+       /* if Shift key is held while activating an action, don't lock shift. */
+       if (_shift_pressed && handled) {
+               _shift_connection.disconnect ();
+               _shift_lock = false;
+       }
+}
+
+void
+FaderPort8::sysex_handler (MIDI::Parser &p, MIDI::byte *buf, size_t size)
+{
+#ifndef NDEBUG
+       if (DEBUG_ENABLED(DEBUG::FaderPort8)) {
+               DEBUG_STR_DECL(a);
+               DEBUG_STR_APPEND(a, string_compose ("RECV sysex siz=%1", size));
+               for (size_t i=0; i < size; ++i) {
+                       DEBUG_STR_APPEND(a,hex);
+                       DEBUG_STR_APPEND(a,"0x");
+                       DEBUG_STR_APPEND(a,(int)buf[i]);
+                       DEBUG_STR_APPEND(a,' ');
+               }
+               DEBUG_STR_APPEND(a,'\n');
+               DEBUG_TRACE (DEBUG::FaderPort8, DEBUG_STR(a).str());
+       }
+#endif
+}
+
+/* ****************************************************************************
+ * User actions
+ */
+void
+FaderPort8::set_button_action (FP8Controls::ButtonId id, bool press, std::string const& action_name)
+{
+       if (_ctrls.user_buttons().find (id) == _ctrls.user_buttons().end ()) {
+               return;
+       }
+       _user_action_map[id].action (press).assign_action (action_name);
+
+       if (!_device_active) {
+               return;
+       }
+       _ctrls.button (id).set_active (!_user_action_map[id].empty ());
+}
+
+std::string
+FaderPort8::get_button_action (FP8Controls::ButtonId id, bool press)
+{
+       return _user_action_map[id].action(press)._action_name;
+}
+
+/* ****************************************************************************
+ * Persistent State
+ */
+XMLNode&
+FaderPort8::get_state ()
+{
+       DEBUG_TRACE (DEBUG::FaderPort8, "FaderPort8::get_state\n");
+       XMLNode& node (ControlProtocol::get_state());
+
+       XMLNode* child;
+
+       child = new XMLNode (X_("Input"));
+       child->add_child_nocopy (boost::shared_ptr<ARDOUR::Port>(_input_port)->get_state());
+       node.add_child_nocopy (*child);
+
+       child = new XMLNode (X_("Output"));
+       child->add_child_nocopy (boost::shared_ptr<ARDOUR::Port>(_output_port)->get_state());
+       node.add_child_nocopy (*child);
+
+       for (UserActionMap::const_iterator i = _user_action_map.begin (); i != _user_action_map.end (); ++i) {
+               if (i->second.empty()) {
+                       continue;
+               }
+               std::string name;
+               if (!_ctrls.button_enum_to_name (i->first, name)) {
+                       continue;
+               }
+               XMLNode* btn = new XMLNode (X_("Button"));
+               btn->add_property (X_("id"), name);
+               if (!i->second.action(true).empty ()) {
+                       btn->add_property ("press", i->second.action(true)._action_name);
+               }
+               if (!i->second.action(false).empty ()) {
+                       btn->add_property ("release", i->second.action(false)._action_name);
+               }
+               node.add_child_nocopy (*btn);
+       }
+
+       return node;
+}
+
+int
+FaderPort8::set_state (const XMLNode& node, int version)
+{
+       DEBUG_TRACE (DEBUG::FaderPort8, "FaderPort8::set_state\n");
+       XMLNodeList nlist;
+       XMLNodeConstIterator niter;
+       XMLNode const* child;
+
+       if (ControlProtocol::set_state (node, version)) {
+               return -1;
+       }
+
+       if ((child = node.child (X_("Input"))) != 0) {
+               XMLNode* portnode = child->child (Port::state_node_name.c_str());
+               if (portnode) {
+                       DEBUG_TRACE (DEBUG::FaderPort8, "FaderPort8::set_state Input\n");
+                       boost::shared_ptr<ARDOUR::Port>(_input_port)->set_state (*portnode, version);
+               }
+       }
+
+       if ((child = node.child (X_("Output"))) != 0) {
+               XMLNode* portnode = child->child (Port::state_node_name.c_str());
+               if (portnode) {
+                       DEBUG_TRACE (DEBUG::FaderPort8, "FaderPort8::set_state Output\n");
+                       boost::shared_ptr<ARDOUR::Port>(_output_port)->set_state (*portnode, version);
+               }
+       }
+
+       _user_action_map.clear ();
+       // TODO: When re-loading state w/o surface re-init becomes possible,
+       // unset lights and reset colors of user buttons.
+
+       for (XMLNodeList::const_iterator n = node.children().begin(); n != node.children().end(); ++n) {
+               if ((*n)->name() != X_("Button")) {
+                       continue;
+               }
+               XMLProperty const* prop = (*n)->property (X_("id"));
+               if (!prop) {
+                       continue;
+               }
+
+               FP8Controls::ButtonId id;
+               if (!_ctrls.button_name_to_enum (prop->value(), id)) {
+                       continue;
+               }
+
+               prop = (*n)->property (X_("press"));
+               if (prop) {
+                       set_button_action (id, true, prop->value());
+               }
+               prop = (*n)->property (X_("release"));
+               if (prop) {
+                       set_button_action (id, false, prop->value());
+               }
+       }
+
+       return 0;
+}
+
+/* ****************************************************************************
+ * Stripable Assignment
+ */
+
+static bool flt_audio_track (boost::shared_ptr<Stripable> s) {
+       return boost::dynamic_pointer_cast<AudioTrack>(s) != 0;
+}
+
+static bool flt_midi_track (boost::shared_ptr<Stripable> s) {
+       return boost::dynamic_pointer_cast<MidiTrack>(s) != 0;
+}
+
+static bool flt_bus (boost::shared_ptr<Stripable> s) {
+       if (boost::dynamic_pointer_cast<Route>(s) == 0) {
+               return false;
+       }
+#ifdef MIXBUS
+       if (s->mixbus () == 0) {
+               return false;
+       }
+#endif
+       return boost::dynamic_pointer_cast<Track>(s) == 0;
+}
+
+static bool flt_auxbus (boost::shared_ptr<Stripable> s) {
+       if (boost::dynamic_pointer_cast<Route>(s) == 0) {
+               return false;
+       }
+#ifdef MIXBUS
+       if (s->mixbus () > 0) {
+               return false;
+       }
+#endif
+       return boost::dynamic_pointer_cast<Track>(s) == 0;
+}
+
+static bool flt_vca (boost::shared_ptr<Stripable> s) {
+       return boost::dynamic_pointer_cast<VCA>(s) != 0;
+}
+
+static bool flt_selected (boost::shared_ptr<Stripable> s) {
+       return s->is_selected ();
+}
+
+static bool flt_mains (boost::shared_ptr<Stripable> s) {
+       return (s->is_master() || s->is_monitor());
+}
+
+static bool flt_all (boost::shared_ptr<Stripable> s) {
+       return true;
+}
+
+static bool flt_rec_armed (boost::shared_ptr<Stripable> s) {
+       boost::shared_ptr<Track> t = boost::dynamic_pointer_cast<Track>(s);
+       if (!t) {
+               return false;
+       }
+       return t->rec_enable_control ()->get_value () > 0.;
+}
+
+static bool flt_instrument (boost::shared_ptr<Stripable> s) {
+       boost::shared_ptr<Route> r = boost::dynamic_pointer_cast<Route>(s);
+       if (!r) {
+               return false;
+       }
+       return 0 != r->the_instrument ();
+}
+
+struct FP8SortByNewDisplayOrder
+{
+       // return (a < b)
+       bool operator () (const boost::shared_ptr<Stripable> & a, const boost::shared_ptr<Stripable> & b) const
+       {
+               if (a->presentation_info().flags () == b->presentation_info().flags ()) {
+                       return a->presentation_info().order() < b->presentation_info().order();
+               }
+
+               int cmp_a = 0;
+               int cmp_b = 0;
+
+               if (a->presentation_info().flags () & ARDOUR::PresentationInfo::VCA) {
+                       cmp_a = 1;
+               }
+#ifdef MIXBUS
+               else if (a->presentation_info().flags () & ARDOUR::PresentationInfo::MasterOut) {
+                       cmp_a = 3;
+               }
+               else if (a->presentation_info().flags () & ARDOUR::PresentationInfo::Mixbus || a->mixbus()) {
+                       cmp_a = 2;
+               }
+#endif
+
+               if (b->presentation_info().flags () & ARDOUR::PresentationInfo::VCA) {
+                       cmp_b = 1;
+               }
+#ifdef MIXBUS
+               else if (b->presentation_info().flags () & ARDOUR::PresentationInfo::MasterOut) {
+                       cmp_b = 3;
+               }
+               else if (b->presentation_info().flags () & ARDOUR::PresentationInfo::Mixbus || b->mixbus()) {
+                       cmp_b = 2;
+               }
+#endif
+
+#ifdef MIXBUS
+               // this can happen with older MB sessions (no PresentationInfo::Mixbus flag)
+               if (cmp_a == cmp_a) {
+                       return a->presentation_info().order() < b->presentation_info().order();
+               }
+#endif
+               return cmp_a < cmp_b;
+       }
+};
+
+void
+FaderPort8::filter_stripables (StripableList& strips) const
+{
+       typedef bool (*FilterFunction)(boost::shared_ptr<Stripable>);
+       FilterFunction flt;
+
+       bool allow_master = false;
+       bool allow_monitor = false;
+
+       switch (_ctrls.mix_mode ()) {
+               case MixAudio:
+                       flt = &flt_audio_track;
+                       break;
+               case MixInstrument:
+                       flt = &flt_instrument;
+                       break;
+               case MixBus:
+                       flt = &flt_bus;
+                       break;
+               case MixVCA:
+                       flt = &flt_vca;
+                       break;
+               case MixMIDI:
+                       flt = &flt_midi_track;
+                       break;
+               case MixUser:
+                       allow_master = true;
+                       flt = &flt_selected;
+                       break;
+               case MixOutputs:
+                       allow_master = true;
+                       allow_monitor = true;
+                       flt = &flt_mains;
+                       break;
+               case MixInputs:
+                       flt = &flt_rec_armed;
+                       break;
+               case MixFX:
+                       flt = &flt_auxbus;
+                       break;
+               case MixAll:
+                       allow_master = true;
+                       flt = &flt_all;
+                       break;
+       }
+
+       StripableList all;
+       session->get_stripables (all);
+
+       for (StripableList::const_iterator s = all.begin(); s != all.end(); ++s) {
+               if ((*s)->is_auditioner ()) { continue; }
+               if ((*s)->is_hidden ()) { continue; }
+
+               if (!allow_master  && (*s)->is_master ()) { continue; }
+               if (!allow_monitor && (*s)->is_monitor ()) { continue; }
+
+               if ((*flt)(*s)) {
+                       strips.push_back (*s);
+               }
+       }
+       strips.sort (FP8SortByNewDisplayOrder());
+}
+
+/* Track/Pan mode: assign stripable to strips */
+void
+FaderPort8::assign_stripables ()
+{
+       StripableList strips;
+       filter_stripables (strips);
+
+       set_periodic_display_mode (FP8Strip::Stripables);
+
+       int n_strips = strips.size();
+       _channel_off = std::min (_channel_off, n_strips - 8);
+       _channel_off = std::max (0, _channel_off);
+
+       uint8_t id = 0;
+       int skip = _channel_off;
+       for (StripableList::const_iterator s = strips.begin(); s != strips.end(); ++s) {
+               if (skip > 0) {
+                       --skip;
+                       continue;
+               }
+
+               _assigned_strips[*s] = id;
+               (*s)->DropReferences.connect (assigned_stripable_connections, MISSING_INVALIDATOR,
+                               boost::bind (&FaderPort8::notify_stripable_added_or_removed, this), this);
+
+               (*s)->PropertyChanged.connect (assigned_stripable_connections, MISSING_INVALIDATOR,
+                               boost::bind (&FaderPort8::notify_stripable_property_changed, this, boost::weak_ptr<Stripable> (*s), _1), this);
+               (*s)->presentation_info ().PropertyChanged.connect (assigned_stripable_connections, MISSING_INVALIDATOR,
+                               boost::bind (&FaderPort8::notify_stripable_property_changed, this, boost::weak_ptr<Stripable> (*s), _1), this);
+
+               _ctrls.strip(id).set_stripable (*s, _ctrls.fader_mode() == ModePan);
+
+                boost::function<void ()> cb (boost::bind (&FaderPort8::select_strip, this, boost::weak_ptr<Stripable> (*s)));
+                _ctrls.strip(id).set_select_cb (cb);
+
+               if (++id == 8) {
+                       break;
+               }
+       }
+       for (; id < 8; ++id) {
+               _ctrls.strip(id).unset_controllables();
+       }
+}
+
+void
+FaderPort8::assign_processor_ctrls ()
+{
+       int n_parameters = _proc_params.size();
+       if (n_parameters == 0) {
+               _ctrls.set_fader_mode (ModeTrack);
+               return;
+       }
+       set_periodic_display_mode (FP8Strip::PluginParam);
+
+       _parameter_off = std::min (_parameter_off, n_parameters - 8);
+       _parameter_off = std::max (0, _parameter_off);
+
+       int skip = _parameter_off;
+       uint8_t id = 0;
+       for ( std::list <ProcessorCtrl>::iterator i = _proc_params.begin(); i != _proc_params.end(); ++i) {
+               if (skip > 0) {
+                       --skip;
+                       continue;
+               }
+               _ctrls.strip(id).unset_controllables (FP8Strip::CTRL_ALL & ~FP8Strip::CTRL_FADER & ~FP8Strip::CTRL_TEXT1);
+               _ctrls.strip(id).set_fader_controllable ((*i).ac);
+               _ctrls.strip(id).set_text_line (0, (*i).name);
+
+                if (++id == 8) {
+                        break;
+                }
+       }
+       // clear remaining
+       for (; id < 8; ++id) {
+               _ctrls.strip(id).unset_controllables ();
+       }
+}
+
+void
+FaderPort8::build_well_known_processor_ctrls (boost::shared_ptr<Stripable> s, bool eq)
+{
+#define PUSH_BACK_NON_NULL(N, C) do {if (C) { _proc_params.push_back (ProcessorCtrl (N, C)); }} while (0)
+
+       _proc_params.clear ();
+       if (eq) {
+               int cnt = s->eq_band_cnt();
+               PUSH_BACK_NON_NULL ("Enable", s->eq_enable_controllable ());
+               PUSH_BACK_NON_NULL ("HPF", s->eq_hpf_controllable ());
+               for (int band = 0; band < cnt; ++band) {
+                       std::string bn = s->eq_band_name (band);
+                       PUSH_BACK_NON_NULL (string_compose ("Gain %1", bn), s->eq_gain_controllable (band));
+                       PUSH_BACK_NON_NULL (string_compose ("Freq %1", bn), s->eq_freq_controllable (band));
+                       PUSH_BACK_NON_NULL (string_compose ("Band %1", bn), s->eq_q_controllable (band));
+                       PUSH_BACK_NON_NULL (string_compose ("Shape %1", bn), s->eq_shape_controllable (band));
+               }
+       } else {
+               PUSH_BACK_NON_NULL ("Enable", s->comp_enable_controllable ());
+               PUSH_BACK_NON_NULL ("Threshold", s->comp_threshold_controllable ());
+               PUSH_BACK_NON_NULL ("Speed", s->comp_speed_controllable ());
+               PUSH_BACK_NON_NULL ("Mode", s->comp_mode_controllable ());
+       }
+}
+
+void
+FaderPort8::select_plugin (int num)
+{
+       boost::shared_ptr<Route> r = boost::dynamic_pointer_cast<Route> (first_selected_stripable());
+       if (!r) {
+               _ctrls.set_fader_mode (ModeTrack);
+               return;
+       }
+       if (num < 0) {
+               build_well_known_processor_ctrls (r, num == -1);
+               assign_processor_ctrls ();
+               return;
+       }
+
+       boost::shared_ptr<Processor> proc = r->nth_plugin (num);
+       if (!proc) {
+               _ctrls.set_fader_mode (ModeTrack);
+               return;
+       }
+
+       // switching to "Mode Track" -> calls FaderPort8::notify_fader_mode_changed()
+       // which drops the references, disconnects the signal and re-spills tracks
+       proc->DropReferences.connect (processor_connections, MISSING_INVALIDATOR, boost::bind (&FP8Controls::set_fader_mode, &_ctrls, ModeTrack), this);
+
+       // build params
+       _proc_params.clear();
+       set<Evoral::Parameter> p = proc->what_can_be_automated ();
+       for (set<Evoral::Parameter>::iterator i = p.begin(); i != p.end(); ++i) {
+               std::string n = proc->describe_parameter (*i);
+               if (n == "hidden") {
+                       continue;
+               }
+               _proc_params.push_back (ProcessorCtrl (n, proc->automation_control (*i)));
+       }
+
+       // display
+       assign_processor_ctrls ();
+}
+
+/* short 4 chars at most */
+static std::string plugintype (ARDOUR::PluginType t) {
+       switch (t) {
+               case AudioUnit:
+                       return "AU";
+               case LADSPA:
+                       return "LV1";
+               case LV2:
+                       return "LV2";
+               case Windows_VST:
+               case LXVST:
+               case MacVST:
+                       return "VST";
+               case Lua:
+                       return "Lua";
+               default:
+                       break;
+       }
+       return enum_2_string (t);
+}
+
+void
+FaderPort8::spill_plugins ()
+{
+       boost::shared_ptr<Route> r = boost::dynamic_pointer_cast<Route> (first_selected_stripable());
+       if (!r) {
+               _ctrls.set_fader_mode (ModeTrack);
+               return;
+       }
+
+       drop_ctrl_connections ();
+
+       // switching to "Mode Track" -> calls FaderPort8::notify_fader_mode_changed()
+       // which drops the references, disconnects the signal and re-spills tracks
+       r->DropReferences.connect (processor_connections, MISSING_INVALIDATOR, boost::bind (&FP8Controls::set_fader_mode, &_ctrls, ModeTrack), this);
+
+       // update when processor change
+       r->processors_changed.connect (processor_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::spill_plugins, this), this);
+
+       // count available
+       boost::shared_ptr<Processor> proc;
+
+       std::vector<uint32_t> procs;
+
+       for (uint32_t i = 0; 0 != (proc = r->nth_plugin (i)); ++i) {
+               if (!proc->display_to_user ()) {
+                       continue;
+               }
+               int n_controls = 0;
+               set<Evoral::Parameter> p = proc->what_can_be_automated ();
+               for (set<Evoral::Parameter>::iterator i = p.begin(); i != p.end(); ++i) {
+                       std::string n = proc->describe_parameter (*i);
+                       if (n == "hidden") {
+                               continue;
+                       }
+                       ++n_controls;
+               }
+               if (n_controls > 0) {
+                       procs.push_back (i);
+               }
+       }
+
+       int n_plugins = procs.size();
+       int spillwidth = 8;
+       bool have_well_known_eq = false;
+       bool have_well_known_comp = false;
+
+       // reserve last slot(s) for "well-known"
+       if (r->eq_band_cnt() > 0) {
+               --spillwidth;
+               have_well_known_eq = true;
+       }
+       if (r->comp_enable_controllable ()) {
+               --spillwidth;
+               have_well_known_comp = true;
+       }
+
+       if (n_plugins == 0 && !have_well_known_eq && !have_well_known_comp) {
+               _ctrls.set_fader_mode (ModeTrack);
+               return;
+       }
+
+       set_periodic_display_mode (FP8Strip::PluginSelect);
+
+       _plugin_off = std::min (_plugin_off, n_plugins - spillwidth);
+       _plugin_off = std::max (0, _plugin_off);
+
+       uint8_t id = 0;
+       for (uint32_t i = _plugin_off; ; ++i) {
+               if (i >= procs.size()) {
+                       break;
+               }
+               boost::shared_ptr<Processor> proc = r->nth_plugin (procs[i]);
+               if (!proc) {
+                       break;
+               }
+               boost::shared_ptr<PluginInsert> pi = boost::dynamic_pointer_cast<PluginInsert> (proc);
+               boost::function<void ()> cb (boost::bind (&FaderPort8::select_plugin, this, i));
+
+               _ctrls.strip(id).unset_controllables (FP8Strip::CTRL_ALL & ~FP8Strip::CTRL_TEXT & ~FP8Strip::CTRL_SELECT);
+               _ctrls.strip(id).set_select_cb (cb);
+               _ctrls.strip(id).select_button ().set_color (0x00ff00ff);
+               _ctrls.strip(id).select_button ().set_active (true /*proc->enabled()*/);
+               _ctrls.strip(id).select_button ().set_blinking (false);
+               _ctrls.strip(id).set_text_line (0, proc->name());
+               _ctrls.strip(id).set_text_line (1, pi->plugin()->maker());
+               _ctrls.strip(id).set_text_line (2, plugintype (pi->type()));
+
+               if (++id == spillwidth) {
+                       break;
+               }
+       }
+       // clear remaining
+       for (; id < spillwidth; ++id) {
+               _ctrls.strip(id).unset_controllables ();
+       }
+
+       if (have_well_known_comp) {
+                       assert (id < 8);
+                boost::function<void ()> cb (boost::bind (&FaderPort8::select_plugin, this, -2));
+                _ctrls.strip(id).unset_controllables (FP8Strip::CTRL_ALL & ~FP8Strip::CTRL_TEXT & ~FP8Strip::CTRL_SELECT);
+                _ctrls.strip(id).set_select_cb (cb);
+                _ctrls.strip(id).select_button ().set_color (0xffff00ff);
+                _ctrls.strip(id).select_button ().set_active (true);
+                _ctrls.strip(id).select_button ().set_blinking (false);
+                _ctrls.strip(id).set_text_line (0, "Comp");
+                _ctrls.strip(id).set_text_line (1, "Built-In");
+                _ctrls.strip(id).set_text_line (2, "--");
+                ++id;
+       }
+       if (have_well_known_eq) {
+                       assert (id < 8);
+                boost::function<void ()> cb (boost::bind (&FaderPort8::select_plugin, this, -1));
+                _ctrls.strip(id).unset_controllables (FP8Strip::CTRL_ALL & ~FP8Strip::CTRL_TEXT & ~FP8Strip::CTRL_SELECT);
+                _ctrls.strip(id).set_select_cb (cb);
+                _ctrls.strip(id).select_button ().set_color (0xffff00ff);
+                _ctrls.strip(id).select_button ().set_active (true);
+                _ctrls.strip(id).select_button ().set_blinking (false);
+                _ctrls.strip(id).set_text_line (0, "EQ");
+                _ctrls.strip(id).set_text_line (1, "Built-In");
+                _ctrls.strip(id).set_text_line (2, "--");
+                ++id;
+       }
+       assert (id == 8);
+}
+
+void
+FaderPort8::assign_sends ()
+{
+       boost::shared_ptr<Stripable> s = first_selected_stripable();
+       if (!s) {
+               _ctrls.set_fader_mode (ModeTrack);
+               return;
+       }
+
+       int n_sends = 0;
+       while (0 != s->send_level_controllable (n_sends)) {
+               ++n_sends;
+       }
+       if (n_sends == 0) {
+               _ctrls.set_fader_mode (ModeTrack);
+               return;
+       }
+
+       drop_ctrl_connections ();
+       s->DropReferences.connect (processor_connections, MISSING_INVALIDATOR, boost::bind (&FP8Controls::set_fader_mode, &_ctrls, ModeTrack), this);
+
+       set_periodic_display_mode (FP8Strip::PluginParam);
+
+       _plugin_off = std::min (_plugin_off, n_sends - 8);
+       _plugin_off = std::max (0, _plugin_off);
+
+       uint8_t id = 0;
+       int skip = _parameter_off;
+       for (uint32_t i = _plugin_off; ; ++i) {
+               if (skip > 0) {
+                       --skip;
+                       continue;
+               }
+               boost::shared_ptr<AutomationControl> send = s->send_level_controllable (i);
+               if (!send) {
+                       break;
+               }
+
+               _ctrls.strip(id).unset_controllables (FP8Strip::CTRL_ALL & ~FP8Strip::CTRL_FADER & ~FP8Strip::CTRL_TEXT1);
+               _ctrls.strip(id).set_fader_controllable (send);
+               _ctrls.strip(id).set_text_line (0, s->send_name (i));
+               //_ctrls.strip(id).set_mute_controllable (s->send_enable_controllable (i)); // XXX TODO MB assign -> select ?
+
+               if (++id == 8) {
+                       break;
+               }
+       }
+       // clear remaining
+       for (; id < 8; ++id) {
+               _ctrls.strip(id).unset_controllables ();
+       }
+}
+
+void
+FaderPort8::set_periodic_display_mode (FP8Strip::DisplayMode m)
+{
+       for (uint8_t id = 0; id < 8; ++id) {
+               _ctrls.strip(id).set_periodic_display_mode (m);
+       }
+}
+
+void
+FaderPort8::assign_strips (bool reset_bank)
+{
+       if (reset_bank) {
+               _channel_off = 0;
+       }
+
+       _assigned_strips.clear ();
+       assigned_stripable_connections.drop_connections ();
+
+       FaderMode fadermode = _ctrls.fader_mode ();
+       switch (fadermode) {
+               case ModeTrack:
+               case ModePan:
+                       assign_stripables ();
+                       gui_track_selection_changed (); // update selection, automation-state
+                       break;
+               case ModePlugins:
+                       if (_proc_params.size() > 0) {
+                               assign_processor_ctrls ();
+                       } else {
+                               spill_plugins ();
+                       }
+                       break;
+               case ModeSend:
+                       assign_sends ();
+                       break;
+       }
+}
+
+
+void
+FaderPort8::drop_ctrl_connections ()
+{
+       _proc_params.clear();
+       processor_connections.drop_connections ();
+}
+
+void
+FaderPort8::notify_fader_mode_changed ()
+{
+       FaderMode fadermode = _ctrls.fader_mode ();
+
+       boost::shared_ptr<Stripable> s = first_selected_stripable();
+       if (!s && (fadermode == ModePlugins || fadermode == ModeSend)) {
+               _ctrls.set_fader_mode (ModeTrack);
+               return;
+       }
+
+       drop_ctrl_connections ();
+
+       switch (fadermode) {
+               case ModeTrack:
+               case ModePan:
+                       break;
+               case ModePlugins:
+               case ModeSend:
+                       _plugin_off = 0;
+                       _parameter_off = 0;
+                       // force unset rec-arm button, see also FaderPort8::button_arm
+                       _ctrls.button (FP8Controls::BtnArm).set_active (false);
+                       ARMButtonChange (false);
+                       break;
+       }
+       assign_strips (false);
+       notify_automation_mode_changed ();
+}
+
+/* ****************************************************************************
+ * Assigned Stripable Callbacks
+ */
+
+void
+FaderPort8::notify_stripable_added_or_removed ()
+{
+       /* called by
+        *  - DropReferences
+        *  - session->RouteAdded
+        *  - PresentationInfo::Change -> Properties::hidden
+        */
+       assign_strips (false);
+}
+
+/* functor for FP8Strip's select button */
+void
+FaderPort8::select_strip (boost::weak_ptr<Stripable> ws)
+{
+       boost::shared_ptr<Stripable> s = ws.lock();
+       if (!s) {
+               return;
+       }
+       if (_shift_pressed) {
+               if (s->is_selected ()) {
+                       RemoveStripableFromSelection (s);
+               } else {
+                       SetStripableSelection (s);
+               }
+               return;
+       }
+       if (s->is_selected () && s != first_selected_stripable ()) {
+               set_first_selected_stripable (s);
+               gui_track_selection_changed ();
+       } else {
+               ToggleStripableSelection (s);
+       }
+}
+
+/* called from static PresentationInfo::Change */
+void
+FaderPort8::notify_pi_property_changed (const PropertyChange& what_changed)
+{
+       if (what_changed.contains (Properties::hidden)) {
+               notify_stripable_added_or_removed ();
+       }
+       if (what_changed.contains (Properties::order)) {
+               notify_stripable_added_or_removed ();
+       }
+       // Properties::selected is handled via StripableSelectionChanged
+}
+
+void
+FaderPort8::notify_stripable_property_changed (boost::weak_ptr<Stripable> ws, const PropertyChange& what_changed)
+{
+       boost::shared_ptr<Stripable> s = ws.lock();
+       if (!s) {
+               assert (0); // this should not happen
+               return;
+       }
+       assert (_assigned_strips.find (s) != _assigned_strips.end());
+       uint8_t id = _assigned_strips[s];
+
+       if (what_changed.contains (Properties::color)) {
+               _ctrls.strip(id).select_button ().set_color (s->presentation_info ().color());
+       }
+
+       if (what_changed.contains (Properties::name)) {
+               _ctrls.strip(id).set_text_line (0, s->name());
+       }
+}
+
+void
+FaderPort8::gui_track_selection_changed (/*ARDOUR::StripableNotificationListPtr*/)
+{
+       automation_state_connections.drop_connections();
+
+       switch (_ctrls.fader_mode ()) {
+               case ModePlugins:
+                       if (_proc_params.size () > 0) {
+                               ; // TODO w/"well-known" -> re-assign to new strip ?!
+                       } else {
+                               notify_fader_mode_changed ();
+                       }
+                       return;
+               case ModeSend:
+                       notify_automation_mode_changed ();
+                       return;
+               default:
+                       break;
+       }
+
+       for (StripAssignmentMap::const_iterator i = _assigned_strips.begin(); i != _assigned_strips.end(); ++i) {
+               boost::shared_ptr<ARDOUR::Stripable> s = i->first;
+               uint8_t id = i->second;
+               bool sel = s->is_selected ();
+               _ctrls.strip(id).select_button ().set_active (sel);
+               _ctrls.strip(id).select_button ().set_blinking (sel && s == first_selected_stripable ());
+       }
+
+       boost::shared_ptr<Stripable> s = first_selected_stripable();
+       if (s) {
+               boost::shared_ptr<AutomationControl> ac;
+               ac = s->gain_control();
+               if (ac && ac->alist()) {
+                       ac->alist()->automation_state_changed.connect (automation_state_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_automation_mode_changed, this), this);
+               }
+               ac = s->pan_azimuth_control();
+               if (ac && ac->alist()) {
+                       ac->alist()->automation_state_changed.connect (automation_state_connections, MISSING_INVALIDATOR, boost::bind (&FaderPort8::notify_automation_mode_changed, this), this);
+               }
+       }
+       notify_automation_mode_changed ();
+}
+
+
+/* ****************************************************************************
+ * Banking
+ */
+
+void
+FaderPort8::move_selected_into_view ()
+{
+       boost::shared_ptr<Stripable> selected = first_selected_stripable ();
+       if (!selected) {
+               return;
+       }
+
+       StripableList strips;
+       filter_stripables (strips);
+
+       StripableList::iterator it = std::find (strips.begin(), strips.end(), selected);
+       if (it == strips.end()) {
+               return;
+       }
+       int off = std::distance (strips.begin(), it);
+
+       if (_channel_off <= off && off < _channel_off + 8) {
+               return;
+       }
+
+       if (_channel_off > off) {
+               _channel_off = off;
+       } else {
+               _channel_off = off - 7;
+       }
+       assign_strips (false);
+}
+
+void
+FaderPort8::bank (bool down, bool page)
+{
+       int dt = page ? 8 : 1;
+       if (down) {
+               dt *= -1;
+       }
+       _channel_off += dt;
+       assign_strips (false);
+}
+
+void
+FaderPort8::bank_param (bool down, bool page)
+{
+       int dt = page ? 8 : 1;
+       if (down) {
+               dt *= -1;
+       }
+       _channel_off += dt;
+       switch (_ctrls.fader_mode ()) {
+               case ModePlugins:
+                       if (_proc_params.size() > 0) {
+                               _parameter_off += dt;
+                               assign_processor_ctrls ();
+                       } else {
+                               _plugin_off += dt;
+                               spill_plugins ();
+                       }
+                       break;
+               default:
+                       break;
+       }
+}
diff --git a/libs/surfaces/faderport8/faderport8.h b/libs/surfaces/faderport8/faderport8.h
new file mode 100644 (file)
index 0000000..64b5b4d
--- /dev/null
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ * Copyright (C) 2015 Paul Davis
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef ardour_surface_faderport8_h
+#define ardour_surface_faderport8_h
+
+#include <list>
+#include <map>
+#include <glibmm/threads.h>
+
+#define ABSTRACT_UI_EXPORTS
+#include "pbd/abstract_ui.h"
+#include "pbd/properties.h"
+
+#include "ardour/types.h"
+#include "ardour/async_midi_port.h"
+#include "ardour/midi_port.h"
+
+#include "control_protocol/control_protocol.h"
+
+#include "fp8_base.h"
+#include "fp8_controls.h"
+
+namespace MIDI {
+       class Parser;
+}
+
+namespace ARDOUR {
+       class Bundle;
+       class Session;
+       class Processor;
+}
+
+namespace ArdourSurface {
+
+struct FaderPort8Request : public BaseUI::BaseRequestObject
+{
+       public:
+               FaderPort8Request () {}
+               ~FaderPort8Request () {}
+};
+
+class FaderPort8 : public FP8Base, public ARDOUR::ControlProtocol, public AbstractUI<FaderPort8Request>
+{
+public:
+       FaderPort8 (ARDOUR::Session&);
+       virtual ~FaderPort8();
+
+       int set_active (bool yn);
+
+       /* we probe for a device when our ports are connected. Before that,
+        * there's no way to know if the device exists or not.
+        */
+       static bool  probe() { return true; }
+       static void* request_factory (uint32_t);
+
+       XMLNode& get_state ();
+       int set_state (const XMLNode&, int version);
+
+       /* configuration GUI */
+       bool  has_editor () const { return true; }
+       void* get_gui () const;
+       void  tear_down_gui ();
+       PBD::Signal0<void> ConnectionChange;
+
+       void set_button_action (FP8Controls::ButtonId, bool, std::string const&);
+       std::string get_button_action (FP8Controls::ButtonId, bool);
+       FP8Controls const& control () const { return  _ctrls; }
+
+       int stop ();
+       void do_request (FaderPort8Request*);
+       void thread_init ();
+
+       boost::shared_ptr<ARDOUR::Port> input_port() const { return _input_port; }
+       boost::shared_ptr<ARDOUR::Port> output_port() const { return _output_port; }
+       std::list<boost::shared_ptr<ARDOUR::Bundle> > bundles ();
+
+       size_t tx_midi (std::vector<uint8_t> const&) const;
+
+private:
+       void close ();
+
+       void start_midi_handling ();
+       void stop_midi_handling ();
+
+       /* I/O Ports */
+       PBD::ScopedConnection port_connection;
+       boost::shared_ptr<ARDOUR::AsyncMIDIPort> _input_port;
+       boost::shared_ptr<ARDOUR::AsyncMIDIPort> _output_port;
+       boost::shared_ptr<ARDOUR::Bundle>        _input_bundle;
+       boost::shared_ptr<ARDOUR::Bundle>        _output_bundle;
+
+       bool midi_input_handler (Glib::IOCondition ioc, boost::weak_ptr<ARDOUR::AsyncMIDIPort> port);
+
+       bool connection_handler (boost::weak_ptr<ARDOUR::Port>, std::string name1, boost::weak_ptr<ARDOUR::Port>, std::string name2, bool yn);
+
+       enum ConnectionState {
+               InputConnected = 0x1,
+               OutputConnected = 0x2
+       };
+
+       void connected ();
+       void disconnected ();
+       int  _connection_state;
+       bool _device_active;
+
+       /* MIDI input message handling */
+       void sysex_handler (MIDI::Parser &p, MIDI::byte *, size_t);
+       void polypressure_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb);
+       void pitchbend_handler (MIDI::Parser &, uint8_t chan, MIDI::pitchbend_t pb);
+       void controller_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb);
+       void note_on_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb);
+       void note_off_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb);
+       PBD::ScopedConnectionList midi_connections;
+
+       /* ***************************************************************************
+        * Control Elements
+        */
+       FP8Controls _ctrls;
+       void notify_stripable_added_or_removed ();
+       void notify_fader_mode_changed ();
+       void filter_stripables (ARDOUR::StripableList& strips) const;
+       void assign_stripables ();
+       void set_periodic_display_mode (FP8Strip::DisplayMode);
+
+       void assign_strips (bool reset_bank);
+       void bank (bool down, bool page);
+       void move_selected_into_view ();
+
+       void assign_sends ();
+       void spill_plugins ();
+       void assign_processor_ctrls ();
+       void build_well_known_processor_ctrls (boost::shared_ptr<ARDOUR::Stripable>, bool);
+       void select_plugin (int num);
+
+       void bank_param (bool down, bool page);
+       /* bank offsets */
+       int _channel_off;
+       int _plugin_off;
+       int _parameter_off;
+
+       /* plugin + send mode stripable
+        *
+        * This is used when parameters of one strip are assigned to
+        * individual FP8Strip controls (Edit Send, Edit Plugins).
+        *
+        * When there's one stripable per FP8Strip, FP8Strip itself keeps
+        * track of the object lifetime and these are NULL.
+        */
+       PBD::ScopedConnectionList processor_connections;
+
+       PBD::ScopedConnectionList assigned_stripable_connections;
+       typedef std::map<boost::shared_ptr<ARDOUR::Stripable>, uint8_t> StripAssignmentMap;
+       StripAssignmentMap _assigned_strips;
+
+       void drop_ctrl_connections ();
+
+       void select_strip (boost::weak_ptr<ARDOUR::Stripable>);
+       void notify_pi_property_changed (const PBD::PropertyChange&);
+       void notify_stripable_property_changed (boost::weak_ptr<ARDOUR::Stripable>, const PBD::PropertyChange&);
+       void gui_track_selection_changed ();
+
+       PBD::ScopedConnection selection_connection;
+       PBD::ScopedConnectionList automation_state_connections;
+       PBD::ScopedConnectionList modechange_connections;
+       /* **************************************************************************/
+       struct ProcessorCtrl {
+               ProcessorCtrl (std::string const &n, boost::shared_ptr<ARDOUR::AutomationControl> c)
+                : name (n)
+                , ac (c)
+               {}
+               std::string name;
+               boost::shared_ptr<ARDOUR::AutomationControl> ac;
+       };
+       std::list <ProcessorCtrl> _proc_params;
+       /* **************************************************************************/
+
+       /* periodic updates, parameter poll */
+       sigc::connection _periodic_connection;
+       bool periodic ();
+       std::string _timecode;
+       std::string const& timecode () const { return _timecode; }
+
+       /* sync button blink -- the FP's blink mode does not work */
+       sigc::connection _blink_connection;
+       bool _blink_onoff;
+       bool blink_it ();
+
+       /* shift key */
+       sigc::connection _shift_connection;
+       bool _shift_lock;
+       bool _shift_pressed;
+       bool shift_timeout () { _shift_lock = true; return false; }
+       bool shift_mod () const { return _shift_lock | _shift_pressed; }
+
+       /* GUI */
+       void build_gui ();
+       mutable void *gui;
+
+       /* setup callbacks & actions */
+       void connect_session_signals ();
+       void setup_actions ();
+       void send_session_state ();
+
+       /* callbacks */
+       PBD::ScopedConnectionList session_connections;
+       void notify_parameter_changed (std::string);
+       void notify_record_state_changed ();
+       void notify_transport_state_changed ();
+       void notify_loop_state_changed ();
+       void notify_snap_change ();
+       void notify_session_dirty_changed ();
+       void notify_history_changed ();
+       void notify_solo_changed ();
+       void notify_mute_changed ();
+       void notify_automation_mode_changed ();
+
+       /* actions */
+       PBD::ScopedConnectionList button_connections;
+       void button_play ();
+       void button_stop ();
+       void button_record ();
+       void button_loop ();
+       void button_metronom ();
+       void button_varispeed (bool);
+       void button_mute_clear ();
+       void button_arm (bool);
+       void button_arm_all ();
+       void button_automation (ARDOUR::AutoState);
+       void button_prev_next (bool);
+       void button_action (const std::string& group, const std::string& item);
+
+       void button_encoder ();
+       void button_parameter ();
+       void encoder_navigate (bool, int);
+       void encoder_parameter (bool, int);
+
+       /* user bound actions */
+       void button_user (bool, FP8Controls::ButtonId);
+
+       enum ActionType {
+               Unset,
+               NamedAction,
+               // InternalFunction, // unused
+       };
+
+       struct UserAction {
+               UserAction () : _type (Unset) {}
+
+               ActionType _type;
+               std::string _action_name;
+               //boost::function<void()> function; // unused
+
+               void clear ()
+               {
+                       _type = Unset;
+                       _action_name.clear();
+               }
+
+               void assign_action (std::string const& action_name)
+               {
+                       if (action_name.empty ()) {
+                               _type = Unset;
+                               _action_name.clear();
+                       } else {
+                               _type = NamedAction;
+                               _action_name = action_name;
+                       }
+               }
+
+               bool empty () const
+               {
+                       return _type == Unset;
+               }
+
+               void call (FaderPort8& _base) const
+               {
+                       switch (_type) {
+                               case NamedAction:
+                                       _base.access_action (_action_name);
+                                       break;
+                               default:
+                                       break;
+                       }
+               }
+       };
+
+       struct ButtonAction {
+               UserAction on_press;
+               UserAction on_release;
+
+               UserAction& action (bool press)
+               {
+                       return press ? on_press : on_release;
+               }
+
+               UserAction const& action (bool press) const
+               {
+                       return press ? on_press : on_release;
+               }
+
+               void call (FaderPort8& _base, bool press) const
+               {
+                       action (press).call (_base);
+               }
+               bool empty () const
+               {
+                       return on_press.empty () && on_release.empty();
+               }
+       };
+
+       typedef std::map<FP8Controls::ButtonId, ButtonAction> UserActionMap;
+       UserActionMap _user_action_map;
+};
+
+} /* namespace */
+
+#endif /* ardour_surface_faderport8_h */
diff --git a/libs/surfaces/faderport8/faderport8_interface.cc b/libs/surfaces/faderport8/faderport8_interface.cc
new file mode 100644 (file)
index 0000000..c87751c
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ * Copyright (C) 2015 Paul Davis
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include <pbd/failed_constructor.h>
+
+#include "control_protocol/control_protocol.h"
+#include "faderport8.h"
+
+using namespace ARDOUR;
+using namespace ArdourSurface;
+
+static ControlProtocol*
+new_faderport8_midi_protocol (ControlProtocolDescriptor* /*descriptor*/, Session* s)
+{
+       FaderPort8* fp;
+
+       try {
+               fp =  new FaderPort8 (*s);
+       } catch (failed_constructor& err) {
+               return 0;
+       }
+
+       if (fp->set_active (true)) {
+               delete fp;
+               return 0;
+       }
+
+       return fp;
+}
+
+static void
+delete_faderport8_midi_protocol (ControlProtocolDescriptor* /*descriptor*/, ControlProtocol* cp)
+{
+       delete cp;
+}
+
+static bool
+probe_faderport8_midi_protocol (ControlProtocolDescriptor* /*descriptor*/)
+{
+       return FaderPort8::probe ();
+}
+
+static void*
+faderport8_request_buffer_factory (uint32_t num_requests)
+{
+       return FaderPort8::request_factory (num_requests);
+}
+
+static ControlProtocolDescriptor faderport8_midi_descriptor = {
+       /*name :              */    "PreSonus FaderPort8",
+       /*id :                */    "uri://ardour.org/surfaces/faderport8:0",
+       /*ptr :               */    0,
+       /*module :            */    0,
+       /*mandatory :         */    0,
+       /*supports_feedback : */    true,
+       /*probe :             */    probe_faderport8_midi_protocol,
+       /*initialize :        */    new_faderport8_midi_protocol,
+       /*destroy :           */    delete_faderport8_midi_protocol,
+       /*request_buffer_factory */ faderport8_request_buffer_factory
+};
+
+extern "C" ARDOURSURFACE_API
+ControlProtocolDescriptor* protocol_descriptor () {
+       return &faderport8_midi_descriptor;
+}
diff --git a/libs/surfaces/faderport8/fp8_base.h b/libs/surfaces/faderport8/fp8_base.h
new file mode 100644 (file)
index 0000000..b0d5df4
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef _ardour_surfaces_fp8base_h_
+#define _ardour_surfaces_fp8base_h_
+
+#include <stdint.h>
+#include <vector>
+
+#include "pbd/signals.h"
+
+namespace ArdourSurface {
+
+/* conveniece wrappers depending on "FP8Base& _base" */
+#define fp8_loop dynamic_cast<BaseUI*>(&_base)->main_loop
+#define fp8_context() dynamic_cast<BaseUI*>(&_base)
+#define fp8_protocol() dynamic_cast<ControlProtocol*>(&_base)
+
+class FP8Base
+{
+public:
+       virtual ~FP8Base() {}
+
+       virtual size_t tx_midi (std::vector<uint8_t> const&) const = 0;
+       virtual std::string const& timecode () const = 0;
+
+       size_t tx_midi2 (uint8_t sb, uint8_t d1) const
+       {
+                std::vector<uint8_t> d;
+                d.push_back (sb);
+                d.push_back (d1);
+                return tx_midi (d);
+       }
+
+       size_t tx_midi3 (uint8_t sb, uint8_t d1, uint8_t d2) const
+       {
+                std::vector<uint8_t> d;
+                d.push_back (sb);
+                d.push_back (d1);
+                d.push_back (d2);
+                return tx_midi (d);
+       }
+
+       size_t tx_sysex (size_t count, ...)
+       {
+                std::vector<uint8_t> d;
+                sysexhdr (d);
+
+                va_list var_args;
+                va_start (var_args, count);
+                for  (size_t i = 0; i < count; ++i)
+                {
+                        // uint8_t {aka unsigned char} is promoted to â€˜int’ when passed through â€˜...’
+                        uint8_t b = va_arg (var_args, int);
+                        d.push_back (b);
+                }
+                va_end (var_args);
+
+                d.push_back (0xf7);
+                return tx_midi (d);
+       }
+
+       size_t tx_text (uint8_t id, uint8_t line, uint8_t align, std::string const& txt)
+       {
+                std::vector<uint8_t> d;
+                sysexhdr (d);
+                d.push_back (0x12);
+                d.push_back (id & 0x07);
+                d.push_back (line & 0x03);
+                d.push_back (align & 0x07);
+
+                for  (size_t i = 0; i < txt.size(); ++i)
+                {
+                        d.push_back (txt[i]);
+                        if (i >= 8) {
+                                break;
+                        }
+                }
+                d.push_back (0xf7);
+                return tx_midi (d);
+       }
+
+       PBD::Signal1<void, bool> ShiftButtonChange;
+       PBD::Signal1<void, bool> ARMButtonChange;
+
+       PBD::Signal1<void, bool> BlinkIt;
+       PBD::Signal0<void> Periodic;
+
+private:
+       void sysexhdr (std::vector<uint8_t>& d)
+       {
+               /* faderport8 <SysExHdr> */
+               d.push_back (0xf0);
+               d.push_back (0x00);
+               d.push_back (0x01);
+               d.push_back (0x06);
+               d.push_back (0x02);
+       }
+};
+
+namespace FP8Types {
+
+       enum FaderMode {
+               ModeTrack,
+               ModePlugins,
+               ModeSend,
+               ModePan
+       };
+
+       enum NavigationMode {
+               NavChannel,
+               NavZoom,
+               NavScroll,
+               NavBank,
+               NavMaster,
+               NavSection,
+               NavMarker
+       };
+
+       enum MixMode {
+               MixAudio,
+               MixInstrument,
+               MixBus,
+               MixVCA,
+               MixAll,
+               MixInputs,
+               MixMIDI,
+               MixOutputs,
+               MixFX,
+               MixUser,
+       };
+
+};
+
+} /* namespace */
+#endif /* _ardour_surfaces_fp8base_h_ */
diff --git a/libs/surfaces/faderport8/fp8_button.h b/libs/surfaces/faderport8/fp8_button.h
new file mode 100644 (file)
index 0000000..a817dd8
--- /dev/null
@@ -0,0 +1,490 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef _ardour_surfaces_fp8button_h_
+#define _ardour_surfaces_fp8button_h_
+
+#include <stdint.h>
+
+#include "pbd/base_ui.h"
+#include "pbd/signals.h"
+
+#include "fp8_base.h"
+
+namespace ArdourSurface {
+
+class FP8ButtonInterface
+{
+public:
+       FP8ButtonInterface () {}
+       virtual ~FP8ButtonInterface () {}
+
+       /* user API */
+       PBD::Signal0<void> pressed;
+       PBD::Signal0<void> released;
+
+       virtual bool is_pressed () const { return false; }
+       virtual bool is_active () const { return false; }
+
+       virtual void ignore_release () {}
+
+       /* internal API - called from midi thread,
+        * user pressed/released button the device
+        */
+       virtual bool midi_event (bool) = 0;
+
+       /* internal API - called from surface thread
+        * set Light on the button
+        */
+       virtual void set_active (bool a) = 0;
+       virtual void set_color (uint32_t rgba) {}
+       virtual void set_blinking (bool) {}
+
+       static bool force_change; // used during init
+};
+
+class FP8DummyButton : public FP8ButtonInterface
+{
+public:
+       virtual void set_active (bool a) {}
+       virtual bool midi_event (bool) { return false; }
+};
+
+
+class FP8ButtonBase : public FP8ButtonInterface
+{
+public:
+       FP8ButtonBase (FP8Base& b)
+               : _base (b)
+               , _pressed (false)
+               , _active (false)
+               , _ignore_release (false)
+               , _rgba (0)
+               , _blinking (false)
+       { }
+
+       bool is_pressed () const { return _pressed; }
+       bool is_active () const { return _active; }
+
+       virtual bool midi_event (bool a)
+       {
+               if (a == _pressed) {
+                       return false;
+               }
+               _pressed = a;
+               if (a) {
+                       pressed (); /* EMIT SIGNAL */
+               } else {
+                       if (_ignore_release) {
+                               _ignore_release = false;
+                       } else {
+                               released (); /* EMIT SIGNAL */
+                       }
+               }
+               return true;
+       }
+
+       void ignore_release () {
+               if (_pressed) {
+                       _ignore_release = true;
+               }
+       }
+
+       void set_blinking (bool yes) {
+               if (yes && !_blinking) {
+                       _blinking = true;
+                       _base.BlinkIt.connect_same_thread (_blink_connection, boost::bind (&FP8ButtonBase::blink, this, _1));
+               } else if (!yes && _blinking) {
+                       _blink_connection.disconnect ();
+                       blink (true);
+                       _blinking = false;
+               }
+       }
+
+protected:
+       FP8Base&              _base;
+       bool                  _pressed;
+       bool                  _active;
+       bool                  _ignore_release;
+       uint32_t              _rgba;
+       virtual void blink (bool onoff) = 0;
+
+private:
+       PBD::ScopedConnection _blink_connection;
+       bool _blinking;
+};
+
+class FP8Button : public FP8ButtonBase
+{
+public:
+       FP8Button (FP8Base& b, uint8_t id, bool color = false)
+               : FP8ButtonBase (b)
+               , _midi_id (id)
+               , _has_color (color)
+       { }
+
+       virtual void set_active (bool a)
+       {
+               if (_active == a && !force_change) {
+                       return;
+               }
+               _active = a;
+               _base.tx_midi3 (0x90, _midi_id, a ? 0x7f : 0x00);
+       }
+
+       void set_color (uint32_t rgba)
+       {
+               if (!_has_color || _rgba == rgba) {
+                       return;
+               }
+               _rgba = rgba;
+               _base.tx_midi3 (0x91, _midi_id, (_rgba >> 25) & 0x7f);
+               _base.tx_midi3 (0x92, _midi_id, (_rgba >> 17) & 0x7f);
+               _base.tx_midi3 (0x93, _midi_id, (_rgba >>  9) & 0x7f);
+       }
+
+protected:
+       void blink (bool onoff)
+       {
+               if (!_active) { return; }
+               _base.tx_midi3 (0x90, _midi_id, onoff ? 0x7f : 0x00);
+       }
+
+       uint8_t  _midi_id; // MIDI-note
+       bool     _has_color;
+};
+
+class FP8ReadOnlyButton : public FP8Button
+{
+public:
+       FP8ReadOnlyButton (FP8Base& b, uint8_t id, bool color = false)
+               : FP8Button (b, id, color)
+       {}
+
+       void set_active (bool) { }
+};
+
+/* virtual button. used for shift toggle. */
+class ShadowButton : public FP8ButtonBase
+{
+public:
+       ShadowButton (FP8Base& b)
+               : FP8ButtonBase (b)
+       {}
+
+       PBD::Signal1<void, bool> ActiveChanged;
+       PBD::Signal0<void> ColourChanged;
+
+       uint32_t color () const { return _rgba; }
+
+       bool midi_event (bool a)
+       {
+               assert (0);
+               return false;
+       }
+
+       bool set_pressed (bool a)
+       {
+               return FP8ButtonBase::midi_event (a);
+       }
+
+       void set_active (bool a)
+       {
+               if (_active == a && !force_change) {
+                       return;
+               }
+               _active = a;
+               ActiveChanged (a); /* EMIT SIGNAL */
+       }
+
+       void set_color (uint32_t rgba)
+       {
+               if (_rgba == rgba) {
+                       return;
+               }
+               _rgba = rgba;
+               ColourChanged ();
+       }
+
+protected:
+       void blink (bool onoff) {
+               if (!_active) { return; }
+               ActiveChanged (onoff);
+       }
+};
+
+/* Wraps 2 buttons with the same physical MIDI ID */
+class FP8DualButton : public FP8ButtonInterface
+{
+public:
+       FP8DualButton (FP8Base& b, uint8_t id, bool color = false)
+               : _base (b)
+               , _b0 (b)
+               , _b1 (b)
+               , _midi_id (id)
+               , _has_color (color)
+               , _rgba (0)
+               , _shift (false)
+       {
+               _b0.ActiveChanged.connect_same_thread (_button_connections, boost::bind (&FP8DualButton::active_changed, this, false, _1));
+               _b1.ActiveChanged.connect_same_thread (_button_connections, boost::bind (&FP8DualButton::active_changed, this, true, _1));
+               if (_has_color) {
+                       _b0.ColourChanged.connect_same_thread (_button_connections, boost::bind (&FP8DualButton::colour_changed, this, false));
+                       _b1.ColourChanged.connect_same_thread (_button_connections, boost::bind (&FP8DualButton::colour_changed, this, true));
+               }
+       }
+
+       bool midi_event (bool a) {
+               return (_shift ? _b1 : _b0).set_pressed (a);
+       }
+
+       void set_active (bool a) {
+               /* This button is never directly used
+                * by the libardour side API.
+                */
+               assert (0);
+       }
+
+       void active_changed (bool s, bool a) {
+               if (s != _shift) {
+                       return;
+               }
+               _base.tx_midi3 (0x90, _midi_id, a ? 0x7f : 0x00);
+       }
+
+       void colour_changed (bool s) {
+               if (s != _shift || !_has_color) {
+                       return;
+               }
+               uint32_t rgba = (_shift ? _b1 : _b0).color ();
+               if (rgba == _rgba) {
+                       return;
+               }
+               _rgba = rgba;
+               _base.tx_midi3 (0x91, _midi_id, (rgba >> 25) & 0x7f);
+               _base.tx_midi3 (0x92, _midi_id, (rgba >> 17) & 0x7f);
+               _base.tx_midi3 (0x93, _midi_id, (rgba >>  9) & 0x7f);
+       }
+
+       FP8ButtonInterface* button () { return &_b0; }
+       FP8ButtonInterface* button_shift () { return &_b1; }
+
+protected:
+       FP8Base&     _base;
+
+       virtual void connect_toggle () = 0;
+
+       void shift_changed (bool shift) {
+               if (_shift == shift) {
+                       return;
+               }
+               (_shift ? _b1 : _b0).set_pressed (false);
+               _shift = shift;
+               active_changed (_shift, (_shift ? _b1 : _b0).is_active());
+               colour_changed (_shift);
+       }
+
+private:
+       ShadowButton _b0;
+       ShadowButton _b1;
+       uint8_t      _midi_id; // MIDI-note
+       bool         _has_color;
+       uint32_t     _rgba;
+       bool         _shift;
+       PBD::ScopedConnectionList _button_connections;
+};
+
+class FP8ShiftSensitiveButton : public FP8DualButton
+{
+public:
+       FP8ShiftSensitiveButton (FP8Base& b, uint8_t id, bool color = false)
+               :FP8DualButton (b, id, color)
+       {
+               connect_toggle ();
+       }
+
+protected:
+       void connect_toggle ()
+       {
+               _base.ShiftButtonChange.connect_same_thread (_shift_connection, boost::bind (&FP8ShiftSensitiveButton::shift_changed, this, _1));
+       }
+
+private:
+       PBD::ScopedConnection _shift_connection;
+};
+
+class FP8ARMSensitiveButton : public FP8DualButton
+{
+public:
+       FP8ARMSensitiveButton (FP8Base& b, uint8_t id, bool color = false)
+               :FP8DualButton (b, id, color)
+       {
+               connect_toggle ();
+       }
+
+protected:
+       void connect_toggle ()
+       {
+               _base.ARMButtonChange.connect_same_thread (_arm_connection, boost::bind (&FP8ARMSensitiveButton::shift_changed, this, _1));
+       }
+
+private:
+       PBD::ScopedConnection _arm_connection;
+};
+
+
+// short press: activate in press, deactivate on release,
+// long press + hold, activate on press, de-activate directly on release
+// e.g. mute/solo  press + hold => changed()
+class FP8MomentaryButton : public FP8ButtonInterface
+{
+public:
+       FP8MomentaryButton (FP8Base& b, uint8_t id)
+               : _base (b)
+               , _midi_id (id)
+               , _pressed (false)
+               , _active (false)
+       {}
+
+       ~FP8MomentaryButton () {
+               _hold_connection.disconnect ();
+       }
+
+       PBD::Signal1<void, bool> StateChange;
+
+       void set_active (bool a)
+       {
+               if (_active == a && !force_change) {
+                       return;
+               }
+               _active = a;
+               _base.tx_midi3 (0x90, _midi_id, a ? 0x7f : 0x00);
+       }
+
+       void reset ()
+       {
+               _was_active_on_press = false;
+               _hold_connection.disconnect ();
+       }
+
+       bool midi_event (bool a)
+       {
+               if (a == _pressed) {
+                       return false;
+               }
+
+               _pressed = a;
+
+               if (a) {
+                       _was_active_on_press = _active;
+               }
+
+               if (a && !_active) {
+                       _momentaty = false;
+                       StateChange (true); /* EMIT SIGNAL */
+                       Glib::RefPtr<Glib::TimeoutSource> hold_timer =
+                               Glib::TimeoutSource::create (500);
+                       hold_timer->attach (fp8_loop()->get_context());
+                       _hold_connection = hold_timer->connect (sigc::mem_fun (*this, &FP8MomentaryButton::hold_timeout));
+               } else if (!a && _was_active_on_press) {
+                       _hold_connection.disconnect ();
+                       _momentaty = false;
+                       StateChange (false); /* EMIT SIGNAL */
+               } else if (!a && _momentaty) {
+                       _hold_connection.disconnect ();
+                       _momentaty = false;
+                       StateChange (false); /* EMIT SIGNAL */
+               }
+               return true;
+       }
+
+protected:
+       FP8Base& _base;
+       uint8_t  _midi_id; // MIDI-note
+       bool     _pressed;
+       bool     _momentaty;
+       bool     _was_active_on_press;
+       bool     _active;
+
+private:
+       bool hold_timeout ()
+       {
+               _momentaty = true;
+               return false;
+       }
+       sigc::connection _hold_connection;
+};
+
+class FP8RepeatButton : public FP8Button
+{
+public:
+       FP8RepeatButton (FP8Base& b, uint8_t id, bool color = false)
+               : FP8Button (b, id, color)
+               , _skip (0)
+       {}
+
+       ~FP8RepeatButton ()
+       {
+               stop_repeat ();
+       }
+
+       bool midi_event (bool a)
+       {
+               bool rv = FP8Button::midi_event (a);
+               if (rv && a) {
+                       start_repeat ();
+               }
+               return rv;
+       }
+
+       void stop_repeat ()
+       {
+               _press_timeout_connection.disconnect ();
+       }
+
+private:
+       void start_repeat ()
+       {
+               stop_repeat ();
+               _skip = 5;
+               Glib::RefPtr<Glib::TimeoutSource> press_timer =
+                       Glib::TimeoutSource::create (100);
+               press_timer->attach (fp8_loop()->get_context());
+               _press_timeout_connection = press_timer->connect (sigc::mem_fun (*this, &FP8RepeatButton::repeat_press));
+       }
+
+       bool repeat_press ()
+       {
+               if (!_pressed) {
+                       return false;
+               }
+               if (_skip > 0) {
+                       --_skip;
+                       return true;
+               }
+               pressed ();
+               return true;
+       }
+
+       int _skip;
+       sigc::connection _press_timeout_connection;
+};
+
+
+} /* namespace */
+#endif /* _ardour_surfaces_fp8button_h_ */
diff --git a/libs/surfaces/faderport8/fp8_controls.cc b/libs/surfaces/faderport8/fp8_controls.cc
new file mode 100644 (file)
index 0000000..5a06dc7
--- /dev/null
@@ -0,0 +1,413 @@
+/* Faderport 8 Control Surface
+ * Abstraction of Surface Control Elements.
+ *
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "fp8_controls.h"
+
+using namespace ArdourSurface;
+using namespace ArdourSurface::FP8Types;
+
+bool FP8ButtonInterface::force_change = false;
+
+#define NEWBUTTON(midi_id, button_id, color)            \
+  do {                                                  \
+  assert (_midimap.end() == _midimap.find (midi_id));   \
+  assert (_ctrlmap.end() == _ctrlmap.find (button_id)); \
+  FP8Button *t = new FP8Button (b, midi_id);            \
+  _midimap[midi_id] = t;                                \
+  _ctrlmap[button_id] = t;                              \
+  } while (0)
+
+
+#define NEWTYPEBUTTON(TYPE, midi_id, button_id, color)  \
+  do {                                                  \
+  assert (_midimap.end() == _midimap.find (midi_id));   \
+  assert (_ctrlmap.end() == _ctrlmap.find (button_id)); \
+  TYPE *t = new TYPE (b, midi_id);                      \
+  _midimap[midi_id] = t;                                \
+  _ctrlmap[button_id] = t;                              \
+  } while (0)
+
+
+
+#define NEWSHIFTBUTTON(midi_id, id1, id2, color)        \
+  do {                                                  \
+  assert (_midimap.end() == _midimap.find (midi_id));   \
+  assert (_ctrlmap.end() == _ctrlmap.find (id1));       \
+  assert (_ctrlmap.end() == _ctrlmap.find (id2));       \
+  FP8ShiftSensitiveButton *t =                          \
+    new FP8ShiftSensitiveButton (b, midi_id, color);    \
+  _midimap[midi_id] = t;                                \
+  _ctrlmap[id1] = t->button ();                         \
+  _ctrlmap[id2] = t->button_shift ();                   \
+  } while (0)
+
+
+FP8Controls::FP8Controls (FP8Base& b)
+       : _fadermode (ModeTrack)
+       , _navmode (NavMaster)
+       , _mixmode (MixAll)
+       , _display_timecode (false)
+{
+       NEWBUTTON (0x56, BtnLoop, false);
+       NEWTYPEBUTTON (FP8RepeatButton, 0x5b, BtnRewind, false);
+       NEWTYPEBUTTON (FP8RepeatButton, 0x5c, BtnFastForward, false);
+       NEWBUTTON (0x5d, BtnStop, false);
+       NEWBUTTON (0x5e, BtnPlay, false);
+       NEWBUTTON (0x5f, BtnRecord, false);
+
+       NEWSHIFTBUTTON (0x4a, BtnARead, BtnUser3, true);
+       NEWSHIFTBUTTON (0x4b, BtnAWrite, BtnUser2, true);
+       NEWSHIFTBUTTON (0x4c, BtnATrim, BtnRedo, true);
+       NEWSHIFTBUTTON (0x4d, BtnATouch, BtnUser1, true);
+       NEWSHIFTBUTTON (0x4e, BtnALatch, BtnSave, true);
+       NEWSHIFTBUTTON (0x4f, BtnAOff, BtnUndo, true);
+
+       NEWBUTTON (0x2e, BtnPrev, false);
+       NEWBUTTON (0x2f, BtnNext, false);
+
+       NEWSHIFTBUTTON (0x36, BtnChannel, BtnF1, false);
+       NEWSHIFTBUTTON (0x37, BtnZoom,    BtnF2, false);
+       NEWSHIFTBUTTON (0x38, BtnScroll,  BtnF3, false);
+       NEWSHIFTBUTTON (0x39, BtnBank,    BtnF4, false);
+       NEWSHIFTBUTTON (0x3a, BtnMaster,  BtnF5, false);
+       NEWSHIFTBUTTON (0x3b, BtnClick,   BtnF6, false);
+       NEWSHIFTBUTTON (0x3c, BtnSection, BtnF7, false);
+       NEWSHIFTBUTTON (0x3d, BtnMarker,  BtnF8, false);
+
+       NEWSHIFTBUTTON (0x28, BtnTrack, BtnTimecode, false);
+       NEWBUTTON (0x2b, BtnPlugins, false);
+       NEWBUTTON (0x29, BtnSend, false);
+       NEWBUTTON (0x2a, BtnPan, false);
+
+       NEWSHIFTBUTTON (0x00, BtnArm, BtnArmAll, false);
+       NEWBUTTON (0x01, BtnSoloClear, false);
+       NEWBUTTON (0x02, BtnMuteClear, false);
+
+       NEWSHIFTBUTTON (0x03, BtnBypass, BtnBypassAll, true);
+       NEWSHIFTBUTTON (0x04, BtnMacro, BtnOpen, true);
+       NEWSHIFTBUTTON (0x05, BtnLink, BtnLock, true);
+
+       NEWSHIFTBUTTON (0x3e, BtnMAudio, BtnMInputs, true);
+       NEWSHIFTBUTTON (0x3f, BtnMVI, BtnMMIDI, true);
+       NEWSHIFTBUTTON (0x40, BtnMBus, BtnMOutputs, true);
+       NEWSHIFTBUTTON (0x41, BtnMVCA, BtnMFX, true);
+       NEWSHIFTBUTTON (0x42, BtnMAll, BtnMUser, true);
+
+       NEWTYPEBUTTON (FP8ReadOnlyButton, 0x53, BtnEncoder, false);
+       NEWTYPEBUTTON (FP8ReadOnlyButton, 0x20, BtnParam, false);
+       NEWTYPEBUTTON (FP8ReadOnlyButton, 0x66, BtnFootswitch, false);
+
+       /* internal bindings */
+
+#define BindMethod(ID, CB) \
+       button (ID).released.connect_same_thread (button_connections, boost::bind (&FP8Controls:: CB, this));
+
+       BindMethod (FP8Controls::BtnTimecode, toggle_timecode);
+
+#define BindNav(BTN, MODE)\
+       button (BTN).released.connect_same_thread (button_connections, boost::bind (&FP8Controls::set_nav_mode, this, MODE))
+
+       BindNav (BtnChannel, NavChannel);
+       BindNav (BtnZoom,    NavZoom);
+       BindNav (BtnScroll,  NavScroll);
+       BindNav (BtnBank,    NavBank);
+       BindNav (BtnMaster,  NavMaster);
+       BindNav (BtnSection, NavSection);
+       BindNav (BtnMarker,  NavMarker);
+
+#define BindFader(BTN, MODE)\
+       button (BTN).released.connect_same_thread (button_connections, boost::bind (&FP8Controls::set_fader_mode, this, MODE))
+
+       BindFader (BtnTrack,   ModeTrack);
+       BindFader (BtnPlugins, ModePlugins);
+       BindFader (BtnSend,    ModeSend);
+       BindFader (BtnPan,     ModePan);
+
+
+#define BindMix(BTN, MODE)\
+       button (BTN).released.connect_same_thread (button_connections, boost::bind (&FP8Controls::set_mix_mode, this, MODE))
+
+       BindMix (BtnMAudio,   MixAudio);
+       BindMix (BtnMVI,      MixInstrument);
+       BindMix (BtnMBus,     MixBus);
+       BindMix (BtnMVCA,     MixVCA);
+       BindMix (BtnMAll,     MixAll);
+       BindMix (BtnMInputs,  MixInputs);
+       BindMix (BtnMMIDI,    MixMIDI);
+       BindMix (BtnMOutputs, MixOutputs);
+       BindMix (BtnMFX,      MixFX);
+       BindMix (BtnMUser,    MixUser);
+
+       /* create channelstrips */
+       for (uint8_t id = 0; id < 8; ++id) {
+               chanstrip[id] = new FP8Strip (b, id);
+               _midimap_strip[0x08 + id] = &(chanstrip[id]->solo_button());
+               _midimap_strip[0x10 + id] = &(chanstrip[id]->mute_button());
+               _midimap_strip[0x18 + id] = &(chanstrip[id]->selrec_button());
+       }
+
+       /* set User button names */
+
+#define REGISTER_ENUM(ID, NAME) \
+       _user_str_to_enum[#ID] = ID; \
+       _user_enum_to_str[ID]  = #ID; \
+       _user_buttons[ID]      = NAME;
+
+       REGISTER_ENUM (BtnFootswitch, "Footswitch");
+       REGISTER_ENUM (BtnUser1     , "User 1");
+       REGISTER_ENUM (BtnUser2     , "User 2");
+       REGISTER_ENUM (BtnUser3     , "User 3");
+       REGISTER_ENUM (BtnF1        , "F1");
+       REGISTER_ENUM (BtnF2        , "F2");
+       REGISTER_ENUM (BtnF3        , "F3");
+       REGISTER_ENUM (BtnF4        , "F4");
+       REGISTER_ENUM (BtnF5        , "F5");
+       REGISTER_ENUM (BtnF6        , "F6");
+       REGISTER_ENUM (BtnF7        , "F7");
+       REGISTER_ENUM (BtnF8        , "F8");
+#undef REGISTER_ENUM
+}
+
+FP8Controls::~FP8Controls ()
+{
+       for (MidiButtonMap::const_iterator i = _midimap.begin (); i != _midimap.end (); ++i) {
+               delete i->second;
+       }
+       for (uint8_t id = 0; id < 8; ++id) {
+               delete chanstrip[id];
+       }
+       _midimap_strip.clear ();
+       _ctrlmap.clear ();
+       _midimap.clear ();
+}
+
+bool
+FP8Controls::button_name_to_enum (std::string const& n, ButtonId& id) const
+{
+       std::map<std::string, ButtonId>::const_iterator i = _user_str_to_enum.find (n);
+       if (i == _user_str_to_enum.end()) {
+               return false;
+       }
+       id = i->second;
+       return true;
+}
+
+bool
+FP8Controls::button_enum_to_name (ButtonId id, std::string& n) const
+{
+       std::map<ButtonId, std::string>::const_iterator i = _user_enum_to_str.find (id);
+       if (i == _user_enum_to_str.end()) {
+               return false;
+       }
+       n = i->second;
+       return true;
+}
+
+void
+FP8Controls::initialize ()
+{
+       FP8ButtonInterface::force_change = true;
+       /* set RGB colors */
+       button (BtnUndo).set_color (0x00ff00ff);
+       button (BtnRedo).set_color (0x00ff00ff);
+
+       button (BtnAOff).set_color (0xffffffff);
+       button (BtnATrim).set_color (0x000030ff);
+       button (BtnARead).set_color (0x00ff00ff);
+       button (BtnAWrite).set_color (0xff0000ff);
+       button (BtnATouch).set_color (0xff8800ff);
+
+       button (BtnUser1).set_color (0x0000ffff);
+       button (BtnUser2).set_color (0x0000ffff);
+       button (BtnUser3).set_color (0x0000ffff);
+
+       button (BtnALatch).set_color (0x0000ffff);
+
+       button (BtnBypass).set_color (0x888888ff);
+       button (BtnBypassAll).set_color (0xffffffff);
+
+       button (BtnMacro).set_color (0x888888ff);
+       button (BtnOpen).set_color (0xffffffff);
+
+       button (BtnLink).set_color (0x888888ff);
+       button (BtnLock).set_color (0xffffffff);
+
+       button (BtnMAudio).set_color (0x0000ffff);
+       button (BtnMVI).set_color (0x0000ffff);
+       button (BtnMBus).set_color (0x0000ffff);
+       button (BtnMVCA).set_color (0x0000ffff);
+       button (BtnMAll).set_color (0x0000ffff);
+
+       button (BtnMInputs).set_color (0x0000ffff);
+       button (BtnMMIDI).set_color (0x0000ffff);
+       button (BtnMOutputs).set_color (0x0000ffff);
+       button (BtnMFX).set_color (0x0000ffff);
+       button (BtnMUser).set_color (0x0000ffff);
+
+       for (uint8_t id = 0; id < 8; ++id) {
+               chanstrip[id]->initialize ();
+       }
+
+       /* initally turn all lights off */
+       for (CtrlButtonMap::const_iterator i = _ctrlmap.begin (); i != _ctrlmap.end (); ++i) {
+               i->second->set_active (false);
+       }
+
+       /* default modes */
+       button (BtnMaster).set_active (true);
+       button (BtnTrack).set_active (true);
+       button (BtnMAll).set_active (true);
+       button (BtnTimecode).set_active (_display_timecode);
+
+       FP8ButtonInterface::force_change = false;
+}
+
+FP8ButtonInterface&
+FP8Controls::button (ButtonId id)
+{
+       CtrlButtonMap::const_iterator i = _ctrlmap.find (id);
+       if (i == _ctrlmap.end()) {
+               assert (0);
+               return _dummy_button;
+       }
+       return *(i->second);
+}
+
+FP8Strip&
+FP8Controls::strip (uint8_t id)
+{
+       assert (id < 8);
+       return *chanstrip[id];
+}
+
+/* *****************************************************************************
+ * Delegate MIDI events
+ */
+
+bool
+FP8Controls::midi_event (uint8_t id, uint8_t val)
+{
+       MidiButtonMap::const_iterator i;
+
+       i = _midimap_strip.find (id);
+       if (i != _midimap_strip.end()) {
+               return i->second->midi_event (val > 0x40);
+       }
+
+       i = _midimap.find (id);
+       if (i != _midimap.end()) {
+               return i->second->midi_event (val > 0x40);
+       }
+       return false;
+}
+
+bool
+FP8Controls::midi_touch (uint8_t id, uint8_t val)
+{
+       assert (id < 8);
+       return chanstrip[id]->midi_touch (val > 0x40);
+}
+
+bool
+FP8Controls::midi_fader (uint8_t id, unsigned short val)
+{
+       assert (id < 8);
+       return chanstrip[id]->midi_fader ((val >> 4) / 1023.f);
+}
+
+/* *****************************************************************************
+ * Internal Model + View for Modes
+ */
+
+void
+FP8Controls::set_nav_mode (NavigationMode m)
+{
+       if (_navmode == m) {
+               return;
+       }
+       // TODO add special-cases:
+       // - master/monitor (blink when button is held + monitor section present)
+       // - "click" hold -> encoder sets click volume, encoder-press toggle rec-only-metro
+       button (BtnChannel).set_active (m == NavChannel);
+       button (BtnZoom).set_active (m == NavZoom);
+       button (BtnScroll).set_active (m == NavScroll);
+       button (BtnBank).set_active (m == NavBank);
+       button (BtnMaster).set_active (m == NavMaster);
+       button (BtnSection).set_active (m == NavSection);
+       button (BtnMarker).set_active (m == NavMarker);
+       _navmode = m;
+}
+
+void
+FP8Controls::set_fader_mode (FaderMode m)
+{
+       if (_fadermode == m) {
+               if (m == ModePlugins || m == ModeSend) {
+                       /* "Edit Plugins" while editing Plugin-params, returns back
+                        * to plugin selection.
+                        * "Sends" button banks through sends.
+                        */
+                       FaderModeChanged ();
+               }
+               return;
+       }
+       // set lights
+       button (BtnTrack).set_active (m == ModeTrack);
+       button (BtnPlugins).set_active (m == ModePlugins);
+       button (BtnSend).set_active (m == ModeSend);
+       button (BtnPan).set_active (m == ModePan);
+       _fadermode = m;
+       FaderModeChanged ();
+}
+
+void
+FP8Controls::set_mix_mode (MixMode m)
+{
+       if (_mixmode == m) {
+               if (m == MixUser || m == MixInputs) {
+                       /* always re-assign:
+                        *  - MixUser: depends on selection
+                        *  - MixInputs: depends on rec-arm
+                        */
+                       MixModeChanged ();
+               }
+               return;
+       }
+       button (BtnMAudio).set_active (m == MixAudio);
+       button (BtnMVI).set_active (m == MixInstrument);
+       button (BtnMBus).set_active (m == MixBus);
+       button (BtnMVCA).set_active (m == MixVCA);
+       button (BtnMAll).set_active (m == MixAll);
+       button (BtnMInputs).set_active (m == MixInputs);
+       button (BtnMMIDI).set_active (m == MixMIDI);
+       button (BtnMOutputs).set_active (m == MixOutputs);
+       button (BtnMFX).set_active (m == MixFX);
+       button (BtnMUser).set_active (m == MixUser);
+
+       _mixmode = m;
+       MixModeChanged ();
+}
+
+void
+FP8Controls::toggle_timecode ()
+{
+       _display_timecode = !_display_timecode;
+       button (BtnTimecode).set_active (_display_timecode);
+}
diff --git a/libs/surfaces/faderport8/fp8_controls.h b/libs/surfaces/faderport8/fp8_controls.h
new file mode 100644 (file)
index 0000000..157435b
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef _ardour_surfaces_fp8controls_h_
+#define _ardour_surfaces_fp8controls_h_
+
+#include <map>
+
+#include "fp8_base.h"
+#include "fp8_button.h"
+#include "fp8_strip.h"
+
+namespace ArdourSurface {
+
+class FP8Controls
+{
+public:
+       FP8Controls (FP8Base&);
+       virtual ~FP8Controls ();
+
+       enum ButtonId {
+               BtnPlay,
+               BtnStop,
+               BtnRecord,
+               BtnLoop,
+               BtnRewind,
+               BtnFastForward,
+
+               BtnALatch,
+               BtnATrim,
+               BtnAOff,
+               BtnATouch,
+               BtnAWrite,
+               BtnARead,
+
+               // Automation
+               BtnSave,
+               BtnRedo,
+               BtnUndo,
+               BtnUser1,
+               BtnUser2,
+               BtnUser3,
+
+               BtnFootswitch,
+
+               // Pan/Param encoder press
+               BtnParam,
+
+               // Navigation
+               BtnPrev,
+               BtnNext,
+               BtnEncoder,
+
+               BtnChannel,
+               BtnZoom,
+               BtnScroll,
+               BtnBank,
+               BtnMaster,
+               BtnClick,
+               BtnSection,
+               BtnMarker,
+
+               BtnF1, BtnF2, BtnF3, BtnF4,
+               BtnF5, BtnF6, BtnF7, BtnF8,
+
+               // FaderMode
+               BtnTrack,
+               BtnPlugins,
+               BtnSend,
+               BtnPan,
+
+               BtnTimecode,
+
+               // Mix Management
+               BtnMAudio,
+               BtnMVI,
+               BtnMBus,
+               BtnMVCA,
+               BtnMAll,
+
+               BtnMInputs,
+               BtnMMIDI,
+               BtnMOutputs,
+               BtnMFX,
+               BtnMUser,
+
+               // General Controls
+               BtnArm,
+               BtnArmAll,
+               BtnSoloClear,
+               BtnMuteClear,
+
+               BtnBypass,
+               BtnBypassAll,
+               BtnMacro,
+               BtnOpen,
+               BtnLink,
+               BtnLock,
+
+       };
+
+       typedef std::map <ButtonId, std::string> UserButtonMap;
+
+       UserButtonMap const& user_buttons () const {
+               return _user_buttons;
+       }
+
+       bool button_name_to_enum (std::string const&, ButtonId&) const;
+       bool button_enum_to_name (ButtonId, std::string&) const;
+
+       PBD::Signal0<void> FaderModeChanged;
+       PBD::Signal0<void> MixModeChanged;
+
+       FP8Types::FaderMode fader_mode () const { return _fadermode; }
+       FP8Types::NavigationMode nav_mode () const { return _navmode; }
+       FP8Types::MixMode mix_mode () const { return _mixmode; }
+       bool display_timecode () const { return _display_timecode; }
+
+       FP8ButtonInterface& button (ButtonId id);
+       FP8Strip& strip (uint8_t id);
+
+       bool midi_event (uint8_t id, uint8_t val);
+       bool midi_touch (uint8_t id, uint8_t val);
+       bool midi_fader (uint8_t id, unsigned short val);
+       void initialize ();
+
+       void set_fader_mode (FP8Types::FaderMode);
+protected:
+       typedef std::map <uint8_t, FP8ButtonInterface*> MidiButtonMap;
+       typedef std::map <ButtonId, FP8ButtonInterface*> CtrlButtonMap;
+
+       void set_nav_mode (FP8Types::NavigationMode);
+       void set_mix_mode (FP8Types::MixMode);
+       void toggle_timecode ();
+
+       MidiButtonMap _midimap;
+       CtrlButtonMap _ctrlmap;
+       MidiButtonMap _midimap_strip;
+
+       FP8Strip* chanstrip[8];
+
+       FP8Types::FaderMode      _fadermode;
+       FP8Types::NavigationMode _navmode;
+       FP8Types::MixMode        _mixmode;
+       bool                     _display_timecode;
+
+       UserButtonMap  _user_buttons;
+       FP8DummyButton _dummy_button;
+
+       std::map<std::string, ButtonId> _user_str_to_enum;
+       std::map<ButtonId, std::string> _user_enum_to_str;
+
+       PBD::ScopedConnectionList button_connections;
+};
+
+} /* namespace */
+#endif /* _ardour_surfaces_fp8controls_h_ */
diff --git a/libs/surfaces/faderport8/fp8_strip.cc b/libs/surfaces/faderport8/fp8_strip.cc
new file mode 100644 (file)
index 0000000..68a9e72
--- /dev/null
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "ardour/automation_control.h"
+#include "ardour/gain_control.h"
+#include "ardour/meter.h"
+#include "ardour/mute_control.h"
+#include "ardour/plugin_insert.h"
+#include "ardour/session.h"
+#include "ardour/solo_control.h"
+#include "ardour/stripable.h"
+#include "ardour/track.h"
+#include "ardour/value_as_string.h"
+
+#include "control_protocol/control_protocol.h"
+
+#include "fp8_strip.h"
+
+using namespace ARDOUR;
+using namespace ArdourSurface;
+using namespace ArdourSurface::FP8Types;
+
+FP8Strip::FP8Strip (FP8Base& b, uint8_t id)
+       : _base (b)
+       , _id (id)
+       , _solo   (b, 0x08 + id)
+       , _mute   (b, 0x10 + id)
+       , _selrec (b, 0x18 + id, true)
+       , _touching (false)
+       , _strip_mode (0)
+       , _bar_mode (0)
+       , _displaymode (Stripables)
+{
+       assert (id < 8);
+
+       _last_fader = 65535;
+       _last_meter = _last_redux = _last_panpos = 0xff;
+
+       _mute.StateChange.connect_same_thread (_button_connections, boost::bind (&FP8Strip::set_mute, this, _1));
+       _solo.StateChange.connect_same_thread (_button_connections, boost::bind (&FP8Strip::set_solo, this, _1));
+       select_button ().released.connect_same_thread (_button_connections, boost::bind (&FP8Strip::set_select, this));
+       recarm_button ().released.connect_same_thread (_button_connections, boost::bind (&FP8Strip::set_recarm, this));
+       b.Periodic.connect_same_thread (_base_connection, boost::bind (&FP8Strip::periodic, this));
+}
+
+FP8Strip::~FP8Strip ()
+{
+       _fader_connection.disconnect ();
+       _mute_connection.disconnect ();
+       _solo_connection.disconnect ();
+       _rec_connection.disconnect ();
+       _pan_connection.disconnect ();
+
+       _fader_ctrl.reset ();
+       _mute_ctrl.reset ();
+       _solo_ctrl.reset ();
+       _rec_ctrl.reset ();
+       _pan_ctrl.reset ();
+
+       _base_connection.disconnect ();
+       _button_connections.drop_connections ();
+}
+
+void
+FP8Strip::initialize ()
+{
+       /* this is called once midi transmission is possible,
+        * ie from FaderPort8::connected()
+        */
+       _solo.set_active (false);
+       _mute.set_active (false);
+
+       /* reset momentary button state */
+       _mute.reset ();
+       _solo.reset ();
+
+       /* clear cached values */
+       _last_fader = 65535;
+       _last_meter = _last_redux = _last_panpos = 0xff;
+
+       select_button ().set_color (0xffffffff);
+       select_button ().set_active (false);
+       select_button ().set_blinking (false);
+
+       recarm_button ().set_active (false);
+       recarm_button ().set_color (0xffffffff);
+
+       set_strip_mode (0, true);
+
+       // force unset txt
+       _last_line[0].clear ();
+       _last_line[1].clear ();
+       _last_line[2].clear ();
+       _last_line[3].clear ();
+       _base.tx_sysex (4, 0x12, _id, 0x00, 0x00);
+       _base.tx_sysex (4, 0x12, _id, 0x01, 0x00);
+       _base.tx_sysex (4, 0x12, _id, 0x02, 0x00);
+       _base.tx_sysex (4, 0x12, _id, 0x03, 0x00);
+
+       set_bar_mode (4); // off
+
+       _base.tx_midi2 (0xd0 + _id, 0); // reset meter
+       _base.tx_midi2 (0xd8 + _id, 0); // reset redux
+
+       _base.tx_midi3 (0xe0 + _id, 0, 0); // fader
+}
+
+
+#define GENERATE_SET_CTRL_FUNCTION(NAME)                                            \
+void                                                                                \
+FP8Strip::set_ ##NAME##_controllable (boost::shared_ptr<AutomationControl> ac)      \
+{                                                                                   \
+  if (_##NAME##_ctrl == ac) {                                                       \
+    return;                                                                         \
+  }                                                                                 \
+  _##NAME##_connection.disconnect();                                                \
+  _##NAME##_ctrl = ac;                                                              \
+                                                                                    \
+  if (ac) {                                                                         \
+    ac->Changed.connect (_##NAME##_connection, MISSING_INVALIDATOR,                 \
+      boost::bind (&FP8Strip::notify_##NAME##_changed, this), fp8_context());       \
+  }                                                                                 \
+  notify_##NAME##_changed ();                                                       \
+}
+
+
+GENERATE_SET_CTRL_FUNCTION (fader)
+GENERATE_SET_CTRL_FUNCTION (mute)
+GENERATE_SET_CTRL_FUNCTION (solo)
+GENERATE_SET_CTRL_FUNCTION (rec)
+GENERATE_SET_CTRL_FUNCTION (pan)
+
+#undef GENERATE_SET_CTRL_FUNCTION
+
+void
+FP8Strip::unset_controllables (int which)
+{
+       _peak_meter = boost::shared_ptr<ARDOUR::PeakMeter>();
+       _redux_ctrl = boost::shared_ptr<ARDOUR::ReadOnlyControl>();
+
+       if (which & CTRL_FADER) {
+               set_fader_controllable (boost::shared_ptr<AutomationControl>());
+       }
+       if (which & CTRL_MUTE) {
+               set_mute_controllable (boost::shared_ptr<AutomationControl>());
+       }
+       if (which & CTRL_SOLO) {
+               set_solo_controllable (boost::shared_ptr<AutomationControl>());
+       }
+       if (which & CTRL_REC) {
+               set_rec_controllable (boost::shared_ptr<AutomationControl>());
+       }
+       if (which & CTRL_PAN) {
+               set_bar_mode (4); // off
+               set_pan_controllable (boost::shared_ptr<AutomationControl>());
+       }
+       if (which & CTRL_SELECT) {
+               _select_plugin_functor.clear ();
+               select_button ().set_color (0xffffffff);
+               select_button ().set_active (false);
+               select_button ().set_blinking (false);
+       }
+       if (which & CTRL_TEXT1) {
+               set_text_line (0x00, "");
+       }
+       if (which & CTRL_TEXT2) {
+               set_text_line (0x01, "");
+       }
+       if (which & CTRL_TEXT3) {
+               set_text_line (0x02, "");
+       }
+       if (which & CTRL_TEXT4) {
+               set_text_line (0x03, "");
+       }
+}
+
+void
+FP8Strip::set_stripable (boost::shared_ptr<Stripable> s, bool panmode)
+{
+       assert (s);
+
+       if (panmode) {
+               set_fader_controllable (s->pan_azimuth_control ());
+       } else {
+               set_fader_controllable (s->gain_control ());
+       }
+       set_pan_controllable (s->pan_azimuth_control ());
+
+       if (s->is_monitor ()) {
+               set_mute_controllable (boost::shared_ptr<AutomationControl>());
+       } else {
+               set_mute_controllable (s->mute_control ());
+       }
+       set_solo_controllable (s->solo_control ());
+
+       if (boost::dynamic_pointer_cast<Track> (s)) {
+               boost::shared_ptr<Track> t = boost::dynamic_pointer_cast<Track>(s);
+               set_rec_controllable (t->rec_enable_control ());
+               recarm_button ().set_color (0xff0000ff);
+       } else {
+               set_rec_controllable (boost::shared_ptr<AutomationControl>());
+               recarm_button ().set_color (0xffffffff);
+               recarm_button ().set_active (false);
+       }
+       _peak_meter = s->peak_meter ();
+       _redux_ctrl = s->comp_redux_controllable ();
+
+       _select_plugin_functor.clear ();
+       select_button ().set_active (s->is_selected ());
+       select_button ().set_color (s->presentation_info ().color());
+       //select_button ().set_blinking (false);
+
+       set_strip_mode (0x05);
+       set_text_line (0x00, s->name ());
+       set_text_line (0x01, _pan_ctrl ? _pan_ctrl->get_user_string () : "");
+       set_text_line (0x02, "");
+       set_text_line (0x03, "");
+}
+
+void
+FP8Strip::set_select_cb (boost::function<void ()>& functor)
+{
+       _select_plugin_functor.clear ();
+       _select_plugin_functor = functor;
+}
+
+/* *****************************************************************************
+ * Parse Strip Specifig MIDI Events
+ */
+
+bool
+FP8Strip::midi_touch (bool t)
+{
+       _touching = t;
+       boost::shared_ptr<AutomationControl> ac = _fader_ctrl;
+       if (!ac) {
+               return false;
+       }
+       if (t) {
+               ac->start_touch (ac->session().transport_frame());
+       } else {
+               ac->stop_touch (true, ac->session().transport_frame());
+       }
+       return true;
+}
+
+bool
+FP8Strip::midi_fader (float val)
+{
+       assert (val >= 0.f && val <= 1.f);
+       if (!_touching) {
+               return false;
+       }
+       boost::shared_ptr<AutomationControl> ac = _fader_ctrl;
+       if (!ac) {
+               return false;
+       }
+       ac->set_value (ac->interface_to_internal (val), PBD::Controllable::UseGroup);
+       return true;
+}
+
+/* *****************************************************************************
+ * Actions from Controller, Update Model
+ */
+
+void
+FP8Strip::set_mute (bool on)
+{
+       if (_mute_ctrl) {
+               if (!_mute_ctrl->touching ()) {
+                       _mute_ctrl->start_touch (_mute_ctrl->session().transport_frame());
+               }
+               _mute_ctrl->set_value (on ? 1.0 : 0.0, PBD::Controllable::UseGroup);
+       }
+}
+
+void
+FP8Strip::set_solo (bool on)
+{
+       if (_solo_ctrl) {
+               if (!_solo_ctrl->touching ()) {
+                       _solo_ctrl->start_touch (_solo_ctrl->session().transport_frame());
+               }
+               _solo_ctrl->set_value (on ? 1.0 : 0.0, PBD::Controllable::UseGroup);
+       }
+}
+
+void
+FP8Strip::set_recarm ()
+{
+       if (_rec_ctrl) {
+               const bool on = !recarm_button().is_active();
+               _rec_ctrl->set_value (on ? 1.0 : 0.0, PBD::Controllable::UseGroup);
+       }
+}
+
+
+void
+FP8Strip::set_select ()
+{
+       if (!_select_plugin_functor.empty ()) {
+               _select_plugin_functor ();
+       }
+}
+
+/* *****************************************************************************
+ * Callbacks from Stripable, Update View
+ */
+
+void
+FP8Strip::notify_fader_changed ()
+{
+       boost::shared_ptr<AutomationControl> ac = _fader_ctrl;
+       if (_touching) {
+               return;
+       }
+       float val = 0;
+       if (ac) {
+               val = ac->internal_to_interface (ac->get_value()) * 16368.f; /* 16 * 1023 */
+       }
+       unsigned short mv = lrintf (val);
+       if (mv == _last_fader) {
+               return;
+       }
+       _last_fader = mv;
+       _base.tx_midi3 (0xe0 + _id, (mv & 0x7f), (mv >> 7) & 0x7f);
+}
+
+void
+FP8Strip::notify_solo_changed ()
+{
+       if (_solo_ctrl) {
+               _solo.set_active (_solo_ctrl->get_value () > 0);
+       } else {
+               _solo.set_active (false);
+       }
+}
+
+void
+FP8Strip::notify_mute_changed ()
+{
+       if (_mute_ctrl) {
+               _mute.set_active (_mute_ctrl->get_value () > 0);
+       } else {
+               _mute.set_active (false);
+       }
+}
+
+void
+FP8Strip::notify_rec_changed ()
+{
+       if (_rec_ctrl) {
+               recarm_button ().set_active (_rec_ctrl->get_value() > 0.);
+       } else {
+               recarm_button ().set_active (false);
+       }
+}
+
+void
+FP8Strip::notify_pan_changed ()
+{
+}
+
+/* *****************************************************************************
+ * Periodic View Updates 
+ */
+
+void
+FP8Strip::periodic_update_fader ()
+{
+       boost::shared_ptr<AutomationControl> ac = _fader_ctrl;
+       if (!ac || _touching) {
+               return;
+       }
+
+       ARDOUR::AutoState state = ac->automation_state();
+       if (state == Touch || state == Play) {
+               notify_fader_changed ();
+       }
+}
+
+void
+FP8Strip::periodic_update_meter ()
+{
+       bool have_meter = false;
+       bool have_panner = false;
+
+       if (_peak_meter) {
+               have_meter = true;
+               float dB = _peak_meter->meter_level (0, MeterMCP);
+               // TODO: deflect meter
+               int val = std::min (127.f, std::max (0.f, 2.f * dB + 127.f));
+               if (val != _last_meter || val > 0) {
+                       _base.tx_midi2 (0xd0 + _id, val & 0x7f); // falls off automatically
+                       _last_meter = val;
+               }
+
+       } else {
+               if (0 != _last_meter) {
+                       _base.tx_midi2 (0xd0 + _id, 0);
+                       _last_meter = 0;
+               }
+       }
+
+       // show redux only if there's a meter, too  (strip display mode 5)
+       if (_peak_meter && _redux_ctrl) {
+               float rx = (1.f - _redux_ctrl->get_parameter ()) * 127.f;
+               // TODO: deflect redux
+               int val = std::min (127.f, std::max (0.f, rx));
+               if (val != _last_redux) {
+                       _base.tx_midi2 (0xd8 + _id, val & 0x7f);
+                       _last_redux = val;
+               }
+       } else {
+               if (0 != _last_redux) {
+                       _base.tx_midi2 (0xd8 + _id, 0);
+                       _last_redux = 0;
+               }
+       }
+
+       if (_displaymode == PluginParam) {
+               set_bar_mode (4); // Off
+               if (_fader_ctrl) {
+                       set_text_line (0x01, value_as_string(_fader_ctrl->desc(), _fader_ctrl->get_value()));
+               } else {
+                       set_text_line (0x01, "");
+               }
+       } else if (_pan_ctrl) {
+               have_panner = true;
+               float panpos = _pan_ctrl->internal_to_interface (_pan_ctrl->get_value());
+               int val = std::min (127.f, std::max (0.f, panpos * 128.f));
+               set_bar_mode (1); // Bipolar
+               if (val != _last_panpos) {
+                       _base.tx_midi3 (0xb0, 0x30 + _id, val & 0x7f);
+                       _last_panpos = val;
+               }
+               set_text_line (0x01, _pan_ctrl->get_user_string ());
+       } else {
+               set_bar_mode (4); // Off
+       }
+
+       if (have_meter && have_panner) {
+               set_strip_mode (5); // small meter mode
+       }
+       else if (have_meter) {
+               set_strip_mode (4); // big meter mode
+       }
+       else if (have_panner) {
+               set_strip_mode (0); // 3 lines of text + value
+       } else {
+               set_strip_mode (0); // 3 lines of text + value
+       }
+}
+
+void
+FP8Strip::set_strip_mode (uint8_t strip_mode, bool clear)
+{
+       if (strip_mode == _strip_mode && !clear) {
+               return;
+       }
+       _strip_mode = strip_mode;
+       _base.tx_sysex (3, 0x13, _id, (_strip_mode & 0x07) | (clear ? 0x10 : 0));
+       //_base.tx_midi3 (0xb0, 0x38 + _id, _bar_mode);
+}
+
+void
+FP8Strip::set_bar_mode (uint8_t bar_mode)
+{
+       if (bar_mode == _bar_mode) {
+               return;
+       }
+       _bar_mode = bar_mode;
+       _base.tx_midi3 (0xb0, 0x38 + _id, bar_mode);
+}
+
+void
+FP8Strip::set_text_line (uint8_t line, std::string const& txt)
+{
+       assert (line < 4);
+       if (_last_line[line] == txt) {
+               return;
+       }
+       _base.tx_text (_id, line, 0x00, txt);
+       _last_line[line] = txt;
+}
+
+void
+FP8Strip::periodic_update_timecode ()
+{
+       if (_id >= 2 && _id < 6) {
+               std::string const& tc = _base.timecode();
+               //" HH:MM:SS:FF"
+               std::string t;
+               if (tc.size () == 12) {
+                       t = tc.substr (1 + (_id - 2) * 3, 2);
+               }
+               set_text_line (0x02, t);
+       }
+}
+
+void
+FP8Strip::periodic ()
+{
+       periodic_update_fader ();
+       periodic_update_meter ();
+       if (_displaymode != PluginSelect) {
+               periodic_update_timecode ();
+       }
+}
diff --git a/libs/surfaces/faderport8/fp8_strip.h b/libs/surfaces/faderport8/fp8_strip.h
new file mode 100644 (file)
index 0000000..fd6d7d1
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef _ardour_surfaces_fp8strip_h_
+#define _ardour_surfaces_fp8strip_h_
+
+#include <stdint.h>
+#include <boost/shared_ptr.hpp>
+
+#include "pbd/signals.h"
+
+#include "fp8_base.h"
+#include "fp8_button.h"
+
+namespace ARDOUR {
+       class Stripable;
+       class AutomationControl;
+       class PeakMeter;
+       class ReadOnlyControl;
+}
+
+namespace ArdourSurface {
+
+class FP8Strip
+{
+public:
+       FP8Strip (FP8Base& b, uint8_t id);
+       ~FP8Strip ();
+
+       FP8ButtonInterface& solo_button () { return _solo; }
+       FP8ButtonInterface& mute_button () { return _mute; }
+       FP8ButtonInterface& selrec_button () { return _selrec; }
+       FP8ButtonInterface& recarm_button () { return *_selrec.button_shift(); }
+       FP8ButtonInterface& select_button () { return *_selrec.button(); }
+
+       bool midi_touch (bool t);
+       bool midi_fader (float val);
+
+       void initialize (); // call only when connected, sends midi
+
+       void set_select_cb (boost::function<void ()>&);
+
+       enum DisplayMode {
+               Stripables,
+               PluginSelect, // no clock display
+               PluginParam, // param value
+       };
+
+       void set_periodic_display_mode (DisplayMode m) {
+               _displaymode = m;
+       }
+
+       // convenience function to call all set_XXX_controllable
+       void set_stripable (boost::shared_ptr<ARDOUR::Stripable>, bool panmode);
+       void set_text_line (uint8_t, std::string const&);
+
+       enum CtrlMask {
+               CTRL_FADER  = 0x001,
+               CTRL_MUTE   = 0x002,
+               CTRL_SOLO   = 0x004,
+               CTRL_REC    = 0x004,
+               CTRL_PAN    = 0x008,
+               CTRL_SELECT = 0x010,
+               CTRL_TEXT1  = 0x100,
+               CTRL_TEXT2  = 0x200,
+               CTRL_TEXT3  = 0x400,
+               CTRL_TEXT4  = 0x800,
+
+               CTRL_TEXT   = 0xf00,
+               CTRL_ALL    = 0xfff,
+       };
+
+       void unset_controllables (int which = CTRL_ALL);
+
+       void set_fader_controllable (boost::shared_ptr<ARDOUR::AutomationControl>);
+       void set_mute_controllable  (boost::shared_ptr<ARDOUR::AutomationControl>);
+       void set_solo_controllable  (boost::shared_ptr<ARDOUR::AutomationControl>);
+       void set_rec_controllable   (boost::shared_ptr<ARDOUR::AutomationControl>);
+       void set_pan_controllable   (boost::shared_ptr<ARDOUR::AutomationControl>);
+
+private:
+       FP8Base&  _base;
+       uint8_t   _id;
+       FP8MomentaryButton _solo;
+       FP8MomentaryButton _mute;
+       FP8ARMSensitiveButton _selrec;
+
+       bool _touching;
+
+       PBD::ScopedConnection _base_connection; // periodic
+       PBD::ScopedConnectionList _button_connections;
+
+       boost::shared_ptr<ARDOUR::Stripable> _stripable;
+
+       boost::shared_ptr<ARDOUR::AutomationControl> _fader_ctrl;
+       boost::shared_ptr<ARDOUR::AutomationControl> _mute_ctrl;
+       boost::shared_ptr<ARDOUR::AutomationControl> _solo_ctrl;
+       boost::shared_ptr<ARDOUR::AutomationControl> _rec_ctrl;
+       boost::shared_ptr<ARDOUR::AutomationControl> _pan_ctrl;
+
+       PBD::ScopedConnection _fader_connection;
+       PBD::ScopedConnection _mute_connection;
+       PBD::ScopedConnection _solo_connection;
+       PBD::ScopedConnection _rec_connection;
+       PBD::ScopedConnection _pan_connection;
+
+       boost::shared_ptr<ARDOUR::PeakMeter> _peak_meter;
+       boost::shared_ptr<ARDOUR::ReadOnlyControl> _redux_ctrl;
+       boost::function<void ()> _select_plugin_functor;
+
+       /* notifications, update view */
+       void notify_fader_changed ();
+       void notify_solo_changed ();
+       void notify_mute_changed ();
+       void notify_rec_changed ();
+       void notify_pan_changed ();
+
+       /* actions, update model */
+       void set_mute (bool);
+       void set_solo (bool);
+       void set_select ();
+       void set_recarm ();
+
+       /* periodic poll, update view */
+       void periodic_update_fader ();
+       void periodic_update_meter ();
+       void periodic_update_timecode ();
+       void periodic ();
+
+       /* cache */
+       unsigned short _last_fader;
+       uint8_t _last_meter;
+       uint8_t _last_redux;
+       uint8_t _last_panpos;
+
+       /* display */
+       void set_strip_mode (uint8_t, bool clear = false);
+       void set_bar_mode (uint8_t);
+
+       uint8_t _strip_mode;
+       uint8_t _bar_mode;
+       DisplayMode _displaymode;
+       std::string _last_line[4];
+};
+
+} /* namespace */
+#endif /* _ardour_surfaces_fp8strip_h_ */
diff --git a/libs/surfaces/faderport8/gui.cc b/libs/surfaces/faderport8/gui.cc
new file mode 100644 (file)
index 0000000..8b8673b
--- /dev/null
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ * Copyright (C) 2015 Paul Davis
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include <gtkmm/alignment.h>
+#include <gtkmm/label.h>
+#include <gtkmm/liststore.h>
+
+#include "pbd/unwind.h"
+#include "pbd/strsplit.h"
+#include "pbd/file_utils.h"
+
+#include "gtkmm2ext/bindings.h"
+#include "gtkmm2ext/gtk_ui.h"
+#include "gtkmm2ext/gui_thread.h"
+#include "gtkmm2ext/utils.h"
+
+#include "ardour/audioengine.h"
+#include "ardour/filesystem_paths.h"
+
+#include "faderport8.h"
+#include "gui.h"
+
+#include "pbd/i18n.h"
+
+using namespace PBD;
+using namespace ARDOUR;
+using namespace ArdourSurface;
+using namespace std;
+using namespace Gtk;
+using namespace Gtkmm2ext;
+
+void*
+FaderPort8::get_gui () const
+{
+       if (!gui) {
+               const_cast<FaderPort8*>(this)->build_gui ();
+       }
+       static_cast<Gtk::VBox*>(gui)->show_all();
+       return gui;
+}
+
+void
+FaderPort8::tear_down_gui ()
+{
+       if (gui) {
+               Gtk::Widget *w = static_cast<Gtk::VBox*>(gui)->get_parent();
+               if (w) {
+                       w->hide();
+                       delete w;
+               }
+       }
+       delete static_cast<FP8GUI*> (gui);
+       gui = 0;
+}
+
+void
+FaderPort8::build_gui ()
+{
+       gui = (void*) new FP8GUI (*this);
+}
+
+/* ****************************************************************************/
+
+FP8GUI::FP8GUI (FaderPort8& p)
+       : fp (p)
+       , table (2, 3)
+       , ignore_active_change (false)
+{
+       set_border_width (12);
+
+       table.set_row_spacings (4);
+       table.set_col_spacings (6);
+       table.set_border_width (12);
+       table.set_homogeneous (false);
+
+       Gtk::Label* l;
+       int row = 0;
+
+       input_combo.pack_start (midi_port_columns.short_name);
+       output_combo.pack_start (midi_port_columns.short_name);
+
+       input_combo.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &FP8GUI::active_port_changed), &input_combo, true));
+       output_combo.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &FP8GUI::active_port_changed), &output_combo, false));
+
+       l = manage (new Gtk::Label);
+       l->set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Incoming MIDI on:")));
+       l->set_alignment (1.0, 0.5);
+       table.attach (*l, 0, 2, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0));
+       table.attach (input_combo, 2, 6, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0), 0, 0);
+       row++;
+
+       l = manage (new Gtk::Label);
+       l->set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Outgoing MIDI on:")));
+       l->set_alignment (1.0, 0.5);
+       table.attach (*l, 0, 2, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0));
+       table.attach (output_combo, 2, 6, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0), 0, 0);
+       row++;
+
+       pack_start (table);
+
+       /* actions */
+       build_available_action_menu ();
+
+       int action_row = 0;
+       int action_col = 0;
+       Gtk::Alignment* align;
+
+       for (FP8Controls::UserButtonMap::const_iterator i = fp.control().user_buttons ().begin ();
+                       i != fp.control().user_buttons ().end (); ++i) {
+               Gtk::ComboBox* user_combo = manage (new Gtk::ComboBox);
+               build_action_combo (*user_combo, i->first);
+               l = manage (new Gtk::Label);
+               l->set_markup (string_compose ("<span weight=\"bold\">%1:</span>", i->second));
+               l->set_alignment (1.0, 0.5);
+               table.attach (*l, 2 * action_col, 2 * action_col + 1, row + action_row, row + action_row + 1, AttachOptions(FILL|EXPAND), AttachOptions (0));
+               align = manage (new Alignment);
+               align->set (0.0, 0.5);
+               align->add (*user_combo);
+               table.attach (*align, 2 * action_col + 1, 2 * action_col + 2, row + action_row, row + action_row + 1, AttachOptions(FILL|EXPAND), AttachOptions (0));
+
+               if (++action_row == 4) {
+                       action_row = 0;
+                       ++action_col;
+               }
+       }
+
+       /* update the port connection combos */
+       update_port_combos ();
+
+       /* catch future changes to connection state */
+       fp.ConnectionChange.connect (connection_change_connection, invalidator (*this), boost::bind (&FP8GUI::connection_handler, this), gui_context());
+}
+
+FP8GUI::~FP8GUI ()
+{
+}
+
+void
+FP8GUI::connection_handler ()
+{
+       PBD::Unwinder<bool> ici (ignore_active_change, true);
+       update_port_combos ();
+}
+
+void
+FP8GUI::update_port_combos ()
+{
+       vector<string> midi_inputs;
+       vector<string> midi_outputs;
+
+       ARDOUR::AudioEngine::instance()->get_ports ("", ARDOUR::DataType::MIDI, ARDOUR::PortFlags (ARDOUR::IsOutput|ARDOUR::IsTerminal), midi_inputs);
+       ARDOUR::AudioEngine::instance()->get_ports ("", ARDOUR::DataType::MIDI, ARDOUR::PortFlags (ARDOUR::IsInput|ARDOUR::IsTerminal), midi_outputs);
+
+       Glib::RefPtr<Gtk::ListStore> input = build_midi_port_list (midi_inputs, true);
+       Glib::RefPtr<Gtk::ListStore> output = build_midi_port_list (midi_outputs, false);
+       bool input_found = false;
+       bool output_found = false;
+       int n;
+
+       input_combo.set_model (input);
+       output_combo.set_model (output);
+
+       Gtk::TreeModel::Children children = input->children();
+       Gtk::TreeModel::Children::iterator i;
+       i = children.begin();
+       ++i; /* skip "Disconnected" */
+
+       for (n = 1;  i != children.end(); ++i, ++n) {
+               string port_name = (*i)[midi_port_columns.full_name];
+               if (fp.input_port()->connected_to (port_name)) {
+                       input_combo.set_active (n);
+                       input_found = true;
+                       break;
+               }
+       }
+
+       if (!input_found) {
+               input_combo.set_active (0); /* disconnected */
+       }
+
+       children = output->children();
+       i = children.begin();
+       ++i; /* skip "Disconnected" */
+
+       for (n = 1;  i != children.end(); ++i, ++n) {
+               string port_name = (*i)[midi_port_columns.full_name];
+               if (fp.output_port()->connected_to (port_name)) {
+                       output_combo.set_active (n);
+                       output_found = true;
+                       break;
+               }
+       }
+
+       if (!output_found) {
+               output_combo.set_active (0); /* disconnected */
+       }
+}
+
+
+Glib::RefPtr<Gtk::ListStore>
+FP8GUI::build_midi_port_list (vector<string> const & ports, bool for_input)
+{
+       Glib::RefPtr<Gtk::ListStore> store = ListStore::create (midi_port_columns);
+       TreeModel::Row row;
+
+       row = *store->append ();
+       row[midi_port_columns.full_name] = string();
+       row[midi_port_columns.short_name] = _("Disconnected");
+
+       for (vector<string>::const_iterator p = ports.begin(); p != ports.end(); ++p) {
+               row = *store->append ();
+               row[midi_port_columns.full_name] = *p;
+               std::string pn = ARDOUR::AudioEngine::instance()->get_pretty_name_by_name (*p);
+               if (pn.empty ()) {
+                       pn = (*p).substr ((*p).find (':') + 1);
+               }
+               row[midi_port_columns.short_name] = pn;
+       }
+
+       return store;
+}
+
+void
+FP8GUI::active_port_changed (Gtk::ComboBox* combo, bool for_input)
+{
+       if (ignore_active_change) {
+               return;
+       }
+
+       TreeModel::iterator active = combo->get_active ();
+       string new_port = (*active)[midi_port_columns.full_name];
+
+       if (new_port.empty()) {
+               if (for_input) {
+                       fp.input_port()->disconnect_all ();
+               } else {
+                       fp.output_port()->disconnect_all ();
+               }
+
+               return;
+       }
+
+       if (for_input) {
+               if (!fp.input_port()->connected_to (new_port)) {
+                       fp.input_port()->disconnect_all ();
+                       fp.input_port()->connect (new_port);
+               }
+       } else {
+               if (!fp.output_port()->connected_to (new_port)) {
+                       fp.output_port()->disconnect_all ();
+                       fp.output_port()->connect (new_port);
+               }
+       }
+}
+
+
+
+void
+FP8GUI::build_available_action_menu ()
+{
+       /* build a model of all available actions (needs to be tree structured
+        * more)
+        */
+
+       available_action_model = TreeStore::create (action_columns);
+
+       vector<string> paths;
+       vector<string> labels;
+       vector<string> tooltips;
+       vector<string> keys;
+       vector<Glib::RefPtr<Gtk::Action> > actions;
+
+       Gtkmm2ext::ActionMap::get_all_actions (paths, labels, tooltips, keys, actions);
+
+       typedef std::map<string,TreeIter> NodeMap;
+       NodeMap nodes;
+       NodeMap::iterator r;
+
+
+       vector<string>::iterator k;
+       vector<string>::iterator p;
+       vector<string>::iterator t;
+       vector<string>::iterator l;
+
+       available_action_model->clear ();
+
+       TreeIter rowp;
+       TreeModel::Row parent;
+
+       /* Disabled item (row 0) */
+
+       rowp = available_action_model->append();
+       parent = *(rowp);
+       parent[action_columns.name] = _("Disabled");
+
+       for (l = labels.begin(), k = keys.begin(), p = paths.begin(), t = tooltips.begin(); l != labels.end(); ++k, ++p, ++t, ++l) {
+
+               TreeModel::Row row;
+               vector<string> parts;
+
+               parts.clear ();
+
+               split (*p, parts, '/');
+
+               if (parts.empty()) {
+                       continue;
+               }
+
+               //kinda kludgy way to avoid displaying menu items as mappable
+               if ( parts[1] == _("Main_menu") )
+                       continue;
+               if ( parts[1] == _("JACK") )
+                       continue;
+               if ( parts[1] == _("redirectmenu") )
+                       continue;
+               if ( parts[1] == _("Editor_menus") )
+                       continue;
+               if ( parts[1] == _("RegionList") )
+                       continue;
+               if ( parts[1] == _("ProcessorMenu") )
+                       continue;
+
+               if ((r = nodes.find (parts[1])) == nodes.end()) {
+
+                       /* top level is missing */
+
+                       TreeIter rowp;
+                       TreeModel::Row parent;
+                       rowp = available_action_model->append();
+                       nodes[parts[1]] = rowp;
+                       parent = *(rowp);
+                       parent[action_columns.name] = parts[1];
+
+                       row = *(available_action_model->append (parent.children()));
+
+               } else {
+
+                       row = *(available_action_model->append ((*r->second)->children()));
+
+               }
+
+               /* add this action */
+
+               if (l->empty ()) {
+                       row[action_columns.name] = *t;
+                       action_map[*t] = *p;
+               } else {
+                       row[action_columns.name] = *l;
+                       action_map[*l] = *p;
+               }
+
+               string path = (*p);
+               /* ControlProtocol::access_action() is not interested in the
+                  legacy "<Actions>/" prefix part of a path.
+               */
+               path = path.substr (strlen ("<Actions>/"));
+
+               row[action_columns.path] = path;
+       }
+}
+
+bool
+FP8GUI::find_action_in_model (const TreeModel::iterator& iter, std::string const& action_path, TreeModel::iterator* found)
+{
+       TreeModel::Row row = *iter;
+       string path = row[action_columns.path];
+
+       if (path == action_path) {
+               *found = iter;
+               return true;
+       }
+
+       return false;
+}
+
+void
+FP8GUI::build_action_combo (Gtk::ComboBox& cb, FP8Controls::ButtonId id)
+{
+       cb.set_model (available_action_model);
+       cb.pack_start (action_columns.name);
+
+       /* set the active "row" to the right value for the current button binding */
+       string current_action = fp.get_button_action (id, false); /* lookup release action */
+
+       if (current_action.empty()) {
+               cb.set_active (0); /* "disabled" */
+       } else {
+               TreeModel::iterator iter = available_action_model->children().end();
+
+               available_action_model->foreach_iter (sigc::bind (sigc::mem_fun (*this, &FP8GUI::find_action_in_model), current_action, &iter));
+
+               if (iter != available_action_model->children().end()) {
+                       cb.set_active (iter);
+               } else {
+                       cb.set_active (0);
+               }
+       }
+       /* bind signal _after_ setting the current value */
+       cb.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &FP8GUI::action_changed), &cb, id));
+}
+
+void
+FP8GUI::action_changed (Gtk::ComboBox* cb, FP8Controls::ButtonId id)
+{
+       TreeModel::const_iterator row = cb->get_active ();
+       string action_path = (*row)[action_columns.path];
+       fp.set_button_action (id, false, action_path);
+}
diff --git a/libs/surfaces/faderport8/gui.h b/libs/surfaces/faderport8/gui.h
new file mode 100644 (file)
index 0000000..20b9ebf
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ * Copyright (C) 2015 Paul Davis
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef __ardour_faderport8_gui_h__
+#define __ardour_faderport8_gui_h__
+
+#include <vector>
+#include <string>
+
+#include <gtkmm/box.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/image.h>
+#include <gtkmm/table.h>
+#include <gtkmm/treestore.h>
+
+namespace Gtk {
+       class CellRendererCombo;
+       class ListStore;
+}
+
+#include "faderport8.h"
+
+namespace ArdourSurface {
+
+class FP8GUI : public Gtk::VBox
+{
+public:
+       FP8GUI (FaderPort8&);
+       ~FP8GUI ();
+
+private:
+       FaderPort8& fp;
+       Gtk::Table table;
+       Gtk::Image image;
+
+       /* port connections */
+       Gtk::ComboBox input_combo;
+       Gtk::ComboBox output_combo;
+
+       void update_port_combos ();
+       void connection_handler ();
+       PBD::ScopedConnection connection_change_connection;
+
+       struct MidiPortColumns : public Gtk::TreeModel::ColumnRecord {
+               MidiPortColumns() {
+                       add (short_name);
+                       add (full_name);
+               }
+               Gtk::TreeModelColumn<std::string> short_name;
+               Gtk::TreeModelColumn<std::string> full_name;
+       };
+
+       MidiPortColumns midi_port_columns;
+       bool ignore_active_change;
+
+       Glib::RefPtr<Gtk::ListStore> build_midi_port_list (std::vector<std::string> const & ports, bool for_input);
+       void active_port_changed (Gtk::ComboBox*,bool for_input);
+
+       /* user actions */
+       void build_available_action_menu ();
+       void build_action_combo (Gtk::ComboBox& cb, FP8Controls::ButtonId id);
+       void action_changed (Gtk::ComboBox* cb, FP8Controls::ButtonId id);
+
+       struct ActionColumns : public Gtk::TreeModel::ColumnRecord {
+               ActionColumns() {
+                       add (name);
+                       add (path);
+               }
+               Gtk::TreeModelColumn<std::string> name;
+               Gtk::TreeModelColumn<std::string> path;
+       };
+
+       ActionColumns action_columns;
+       Glib::RefPtr<Gtk::TreeStore> available_action_model;
+       std::map<std::string,std::string> action_map; // map from action names to paths
+
+       bool find_action_in_model (const Gtk::TreeModel::iterator& iter, std::string const & action_path, Gtk::TreeModel::iterator* found);
+};
+
+}
+
+#endif /* __ardour_faderport8_gui_h__ */
diff --git a/libs/surfaces/faderport8/wscript b/libs/surfaces/faderport8/wscript
new file mode 100644 (file)
index 0000000..da2fb8e
--- /dev/null
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+from waflib.extras import autowaf as autowaf
+import os
+
+# Mandatory variables
+top = '.'
+out = 'build'
+
+def options(opt):
+    autowaf.set_options(opt)
+
+def configure(conf):
+    autowaf.configure(conf)
+
+def build(bld):
+    obj = bld(features = 'cxx cxxshlib')
+    obj.source = [
+            'faderport8.cc',
+            'faderport8_interface.cc',
+            'fp8_controls.cc',
+            'fp8_strip.cc',
+            'callbacks.cc',
+            'actions.cc',
+            'gui.cc'
+    ]
+    obj.export_includes = ['.']
+    obj.defines      = [ 'PACKAGE="ardour_faderport8"' ]
+    obj.defines     += [ 'ARDOURSURFACE_DLL_EXPORTS' ]
+    obj.includes     = [ '.', './faderport8']
+    obj.name         = 'libardour_faderport8'
+    obj.target       = 'ardour_faderport8'
+    obj.uselib       = 'GTKMM GTK GDK XML'
+    obj.use          = 'libardour libardour_cp libgtkmm2ext libpbd'
+    obj.install_path = os.path.join(bld.env['LIBDIR'], 'surfaces')
+
+def shutdown():
+    autowaf.shutdown()
index 7ab04ef9f8fc82c91390117dd722d83842a902a6..0e34356de15e9723e7242366b0f11bfcf8fdd86b 100644 (file)
@@ -22,6 +22,7 @@ out = 'build'
 children = [
         'control_protocol',
         'faderport',
+        'faderport8',
         'cc121',
         'generic_midi',
         'mackie',
@@ -75,6 +76,7 @@ def build(bld):
     bld.recurse('control_protocol')
     bld.recurse('generic_midi')
     bld.recurse('faderport')
+    bld.recurse('faderport8')
     bld.recurse('cc121')
     bld.recurse('mackie')