diff options
author | Robin Gareus <robin@gareus.org> | 2017-04-05 11:04:16 +0200 |
---|---|---|
committer | Robin Gareus <robin@gareus.org> | 2017-04-13 21:21:59 +0200 |
commit | d43a23fe28c6f54ed8ca6cde6497661c6cb73ce0 (patch) | |
tree | d89166f438d4a3d42034d6b106506cb892d2f068 | |
parent | d64ca9be08331756e936018ea4d06404faa2ca90 (diff) |
Faderport8 control surface support
-rw-r--r-- | gtk2_ardour/ardev_common.sh.in | 2 | ||||
-rw-r--r-- | libs/ardour/ardour/debug.h | 1 | ||||
-rw-r--r-- | libs/ardour/debug.cc | 1 | ||||
-rw-r--r-- | libs/ardour/port_manager.cc | 1 | ||||
-rw-r--r-- | libs/surfaces/faderport8/actions.cc | 470 | ||||
-rw-r--r-- | libs/surfaces/faderport8/callbacks.cc | 206 | ||||
-rw-r--r-- | libs/surfaces/faderport8/faderport8.cc | 1501 | ||||
-rw-r--r-- | libs/surfaces/faderport8/faderport8.h | 334 | ||||
-rw-r--r-- | libs/surfaces/faderport8/faderport8_interface.cc | 81 | ||||
-rw-r--r-- | libs/surfaces/faderport8/fp8_base.h | 151 | ||||
-rw-r--r-- | libs/surfaces/faderport8/fp8_button.h | 490 | ||||
-rw-r--r-- | libs/surfaces/faderport8/fp8_controls.cc | 413 | ||||
-rw-r--r-- | libs/surfaces/faderport8/fp8_controls.h | 172 | ||||
-rw-r--r-- | libs/surfaces/faderport8/fp8_strip.cc | 524 | ||||
-rw-r--r-- | libs/surfaces/faderport8/fp8_strip.h | 162 | ||||
-rw-r--r-- | libs/surfaces/faderport8/gui.cc | 424 | ||||
-rw-r--r-- | libs/surfaces/faderport8/gui.h | 98 | ||||
-rw-r--r-- | libs/surfaces/faderport8/wscript | 37 | ||||
-rw-r--r-- | libs/surfaces/wscript | 2 |
19 files changed, 5069 insertions, 1 deletions
diff --git a/gtk2_ardour/ardev_common.sh.in b/gtk2_ardour/ardev_common.sh.in index b785e36a10..06dc9d0973 100644 --- a/gtk2_ardour/ardev_common.sh.in +++ b/gtk2_ardour/ardev_common.sh.in @@ -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:. diff --git a/libs/ardour/ardour/debug.h b/libs/ardour/ardour/debug.h index 0b7e5fefb5..eff8305bf1 100644 --- a/libs/ardour/ardour/debug.h +++ b/libs/ardour/ardour/debug.h @@ -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; diff --git a/libs/ardour/debug.cc b/libs/ardour/debug.cc index 8c88dd4d1d..5c645a8095 100644 --- a/libs/ardour/debug.cc +++ b/libs/ardour/debug.cc @@ -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"); diff --git a/libs/ardour/port_manager.cc b/libs/ardour/port_manager.cc index b99337a891..aa56ab8d43 100644 --- a/libs/ardour/port_manager.cc +++ b/libs/ardour/port_manager.cc @@ -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 index 0000000000..949d19a958 --- /dev/null +++ b/libs/surfaces/faderport8/actions.cc @@ -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 index 0000000000..72aa8228bf --- /dev/null +++ b/libs/surfaces/faderport8/callbacks.cc @@ -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 index 0000000000..424742aef0 --- /dev/null +++ b/libs/surfaces/faderport8/faderport8.cc @@ -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 index 0000000000..64b5b4decc --- /dev/null +++ b/libs/surfaces/faderport8/faderport8.h @@ -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 index 0000000000..c87751cf73 --- /dev/null +++ b/libs/surfaces/faderport8/faderport8_interface.cc @@ -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 index 0000000000..b0d5df4923 --- /dev/null +++ b/libs/surfaces/faderport8/fp8_base.h @@ -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 index 0000000000..a817dd8e79 --- /dev/null +++ b/libs/surfaces/faderport8/fp8_button.h @@ -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 index 0000000000..5a06dc75e5 --- /dev/null +++ b/libs/surfaces/faderport8/fp8_controls.cc @@ -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 index 0000000000..157435b059 --- /dev/null +++ b/libs/surfaces/faderport8/fp8_controls.h @@ -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 index 0000000000..68a9e7262b --- /dev/null +++ b/libs/surfaces/faderport8/fp8_strip.cc @@ -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 index 0000000000..fd6d7d10dd --- /dev/null +++ b/libs/surfaces/faderport8/fp8_strip.h @@ -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 index 0000000000..8b8673bb3b --- /dev/null +++ b/libs/surfaces/faderport8/gui.cc @@ -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 index 0000000000..20b9ebfedf --- /dev/null +++ b/libs/surfaces/faderport8/gui.h @@ -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 index 0000000000..da2fb8e300 --- /dev/null +++ b/libs/surfaces/faderport8/wscript @@ -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() diff --git a/libs/surfaces/wscript b/libs/surfaces/wscript index 7ab04ef9f8..0e34356de1 100644 --- a/libs/surfaces/wscript +++ b/libs/surfaces/wscript @@ -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') |