diff --git a/gtk2_ardour/icons/cc121.png b/gtk2_ardour/icons/cc121.png new file mode 100644 index 0000000000..098ed8eb11 Binary files /dev/null and b/gtk2_ardour/icons/cc121.png differ diff --git a/libs/ardour/ardour/debug.h b/libs/ardour/ardour/debug.h index 0c259b480f..0b7e5fefb5 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 CC121; LIBARDOUR_API extern DebugBits VCA; LIBARDOUR_API extern DebugBits Push2; diff --git a/libs/ardour/debug.cc b/libs/ardour/debug.cc index f52181611d..8c88dd4d1d 100644 --- a/libs/ardour/debug.cc +++ b/libs/ardour/debug.cc @@ -77,5 +77,6 @@ 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::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/surfaces/cc121/cc121.cc b/libs/surfaces/cc121/cc121.cc new file mode 100644 index 0000000000..5e68b7b446 --- /dev/null +++ b/libs/surfaces/cc121/cc121.cc @@ -0,0 +1,1290 @@ +/* + Copyright (C) 2015 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + Thanks to Rolf Meyerhoff for reverse engineering the CC121 protocol. + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include +#include +#include + +#include + +#include +#include + +#include "pbd/error.h" +#include "pbd/failed_constructor.h" +#include "pbd/file_utils.h" +#include "pbd/pthread_utils.h" +#include "pbd/compose.h" +#include "pbd/xml++.h" + +#include "midi++/port.h" + +#include "control_protocol/basic_ui.h" + +#include "ardour/async_midi_port.h" +#include "ardour/audioengine.h" +#include "ardour/amp.h" +#include "ardour/bundle.h" +#include "ardour/controllable_descriptor.h" +#include "ardour/debug.h" +#include "ardour/filesystem_paths.h" +#include "ardour/midi_port.h" +#include "ardour/midiport_manager.h" +#include "ardour/monitor_processor.h" +#include "ardour/profile.h" +#include "ardour/rc_configuration.h" +#include "ardour/record_enable_control.h" +#include "ardour/stripable.h" +#include "ardour/session.h" +#include "ardour/session_configuration.h" +#include "ardour/track.h" + +#include "cc121.h" + +using namespace ARDOUR; +using namespace ArdourSurface; +using namespace PBD; +using namespace Glib; +using namespace std; + +#include "pbd/i18n.h" + +#include "pbd/abstract_ui.cc" // instantiate template + +CC121::CC121 (Session& s) + : ControlProtocol (s, _("Steinberg CC121")) + , AbstractUI (name()) + , gui (0) + , connection_state (ConnectionState (0)) + , _device_active (false) + , fader_msb (0) + , fader_lsb (0) + , fader_is_touched (false) + , _jogmode(scroll) + , button_state (ButtonState (0)) + , blink_state (false) + , rec_enable_state (false) +{ + last_encoder_time = 0; + + boost::shared_ptr inp; + boost::shared_ptr outp; + + inp = AudioEngine::instance()->register_input_port (DataType::MIDI, "CC121 Recv", true); + outp = AudioEngine::instance()->register_output_port (DataType::MIDI, "CC121 Send", true); + + _input_port = boost::dynamic_pointer_cast(inp); + _output_port = boost::dynamic_pointer_cast(outp); + + if (_input_port == 0 || _output_port == 0) { + throw failed_constructor(); + } + + _input_bundle.reset (new ARDOUR::Bundle (_("CC121 Support (Receive)"), true)); + _output_bundle.reset (new ARDOUR::Bundle (_("CC121 Support (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()) + ); + + + StripableSelectionChanged.connect (selection_connection, MISSING_INVALIDATOR, boost::bind (&CC121::gui_track_selection_changed, this, _1), this); + + /* Catch port connections and disconnections */ + ARDOUR::AudioEngine::instance()->PortConnectedOrDisconnected.connect (port_connection, MISSING_INVALIDATOR, boost::bind (&CC121::connection_handler, this, _1, _2, _3, _4, _5), this); + buttons.insert (std::make_pair (EButton, Button (*this, _("EButton"), EButton))); + buttons.insert (std::make_pair (OpenVST, Button (*this, _("OpenVST"), OpenVST))); + buttons.insert (std::make_pair (InputMonitor, Button (*this, _("InputMonitor"), InputMonitor))); + buttons.insert (std::make_pair (EQ1Enable, Button (*this, _("EQ1Enable"), EQ1Enable))); + buttons.insert (std::make_pair (EQ2Enable, Button (*this, _("EQ2Enable"), EQ2Enable))); + buttons.insert (std::make_pair (EQ3Enable, Button (*this, _("EQ3Enable"), EQ3Enable))); + buttons.insert (std::make_pair (EQ4Enable, Button (*this, _("EQ4Enable"), EQ4Enable))); + buttons.insert (std::make_pair (EQType, Button (*this, _("EQType"), EQType))); + buttons.insert (std::make_pair (AllBypass, Button (*this, _("AllBypass"), AllBypass))); + buttons.insert (std::make_pair (Function1, Button (*this, _("Function1"), Function1))); + buttons.insert (std::make_pair (Function2, Button (*this, _("Function2"), Function2))); + buttons.insert (std::make_pair (Function3, Button (*this, _("Function3"), Function3))); + buttons.insert (std::make_pair (Function4, Button (*this, _("Function4"), Function4))); + buttons.insert (std::make_pair (Value, Button (*this, _("Value"), Value))); + buttons.insert (std::make_pair (Jog, Button (*this, _("Jog"), Jog))); + buttons.insert (std::make_pair (Lock, Button (*this, _("Lock"), Lock))); + buttons.insert (std::make_pair (ToStart, Button (*this, _("ToStart"), ToStart))); + buttons.insert (std::make_pair (ToEnd, Button (*this, _("ToEnd"), ToEnd))); + buttons.insert (std::make_pair (Mute, Button (*this, _("Mute"), Mute))); + buttons.insert (std::make_pair (Solo, Button (*this, _("Solo"), Solo))); + buttons.insert (std::make_pair (Rec, Button (*this, _("Rec"), Rec))); + buttons.insert (std::make_pair (Left, Button (*this, _("Left"), Left))); + buttons.insert (std::make_pair (Right, Button (*this, _("Right"), Right))); + buttons.insert (std::make_pair (Output, Button (*this, _("Output"), Output))); + buttons.insert (std::make_pair (FP_Read, Button (*this, _("Read"), FP_Read))); + buttons.insert (std::make_pair (FP_Write, Button (*this, _("Write"), FP_Write))); + buttons.insert (std::make_pair (Loop, Button (*this, _("Loop"), Loop))); + buttons.insert (std::make_pair (Rewind, Button (*this, _("Rewind"), Rewind))); + buttons.insert (std::make_pair (Ffwd, Button (*this, _("Ffwd"), Ffwd))); + buttons.insert (std::make_pair (Stop, Button (*this, _("Stop"), Stop))); + buttons.insert (std::make_pair (Play, Button (*this, _("Play"), Play))); + buttons.insert (std::make_pair (RecEnable, Button (*this, _("RecEnable"), RecEnable))); + buttons.insert (std::make_pair (Footswitch, Button (*this, _("Footswitch"), Footswitch))); + buttons.insert (std::make_pair (FaderTouch, Button (*this, _("Fader (touch)"), FaderTouch))); + + get_button (Left).set_action ( boost::bind (&CC121::left, this), true); + get_button (Right).set_action ( boost::bind (&CC121::right, this), true); + + get_button (FP_Read).set_action (boost::bind (&CC121::read, this), true); + get_button (FP_Write).set_action (boost::bind (&CC121::write, this), true); + get_button (EButton).set_action (boost::bind (&CC121::touch, this), true); + get_button (OpenVST).set_action (boost::bind (&CC121::off, this), true); + + get_button (Play).set_action (boost::bind (&BasicUI::transport_play, this, true), true); + get_button (ToStart).set_action (boost::bind (&BasicUI::prev_marker, this), true); + get_button (ToEnd).set_action (boost::bind (&BasicUI::next_marker, this), true); + get_button (RecEnable).set_action (boost::bind (&BasicUI::rec_enable_toggle, this), true); + get_button (Stop).set_action (boost::bind (&BasicUI::transport_stop, this), true); + get_button (Ffwd).set_action (boost::bind (&BasicUI::ffwd, this), true); + + get_button (Rewind).set_action (boost::bind (&BasicUI::rewind, this), true); + get_button (Loop).set_action (boost::bind (&BasicUI::loop_toggle, this), true); + + get_button (Jog).set_action (boost::bind (&CC121::jog, this), true); + get_button (Mute).set_action (boost::bind (&CC121::mute, this), true); + get_button (Solo).set_action (boost::bind (&CC121::solo, this), true); + get_button (Rec).set_action (boost::bind (&CC121::rec_enable, this), true); + + get_button (InputMonitor).set_action (boost::bind (&CC121::input_monitor, this), true); +} + +CC121::~CC121 () +{ + all_lights_out (); + + if (_input_port) { + DEBUG_TRACE (DEBUG::CC121, string_compose ("unregistering input port %1\n", boost::shared_ptr(_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::CC121, string_compose ("unregistering output port %1\n", boost::shared_ptr(_output_port)->name())); + AudioEngine::instance()->unregister_port (_output_port); + _output_port.reset (); + } + + tear_down_gui (); + + /* stop event loop */ + DEBUG_TRACE (DEBUG::CC121, "BaseUI::quit ()\n"); + BaseUI::quit (); +} + +void* +CC121::request_factory (uint32_t num_requests) +{ + /* AbstractUI::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 +CC121::start_midi_handling () +{ + /* handle buttons press */ + _input_port->parser()->channel_note_on[0].connect_same_thread (midi_connections, boost::bind (&CC121::button_press_handler, this, _1, _2)); + /* handle buttons release*/ + _input_port->parser()->channel_note_off[0].connect_same_thread (midi_connections, boost::bind (&CC121::button_release_handler, this, _1, _2)); + /* handle fader */ + _input_port->parser()->pitchbend.connect_same_thread (midi_connections, boost::bind (&CC121::fader_handler, this, _1, _2)); + /* handle encoder */ + _input_port->parser()->controller.connect_same_thread (midi_connections, boost::bind (&CC121::encoder_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, &CC121::midi_input_handler), _input_port)); + _input_port->xthread().attach (main_loop()->get_context()); +} + +void +CC121::stop_midi_handling () +{ + 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 +CC121::do_request (CC121Request* req) +{ + if (req->type == CallSlot) { + + call_slot (MISSING_INVALIDATOR, req->the_slot); + + } else if (req->type == Quit) { + + stop (); + } +} + +int +CC121::stop () +{ + BaseUI::quit (); + + return 0; +} + +void +CC121::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. + } +} + +void +CC121::all_lights_out () +{ + for (ButtonMap::iterator b = buttons.begin(); b != buttons.end(); ++b) { + b->second.set_led_state (_output_port, false); + } +} + +CC121::Button& +CC121::get_button (ButtonID id) const +{ + ButtonMap::const_iterator b = buttons.find (id); + assert (b != buttons.end()); + return const_cast(b->second); +} + +void +CC121::button_press_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb) +{ + DEBUG_TRACE (DEBUG::CC121, string_compose ("button press event for ID %1 press ? %2\n", (int) tb->controller_number, (tb->value ? "yes" : "no"))); + + ButtonID id (ButtonID (tb->controller_number)); + Button& button (get_button (id)); + + buttons_down.insert (id); + ButtonState bs (ButtonState (0)); + + switch (id) { + case FaderTouch: + fader_is_touched = true; + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + framepos_t now = session->engine().sample_time(); + gain->start_touch (now); + } + } + break; + default: + break; + } + + if (bs) { + button_state = ButtonState (button_state|bs); + DEBUG_TRACE (DEBUG::CC121, string_compose ("reset button state to %1 using %2\n", button_state, (int) bs)); + } + + if (button.uses_flash()) { + button.set_led_state (_output_port, (int)tb->value); + } + + set::iterator c = consumed.find (id); + + if (c == consumed.end()) { + button.invoke (button_state, true); + } else { + DEBUG_TRACE (DEBUG::CC121, "button was consumed, ignored\n"); + consumed.erase (c); + } +} + +void +CC121::button_release_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb) +{ + DEBUG_TRACE (DEBUG::CC121, string_compose ("button release event for ID %1 release ? %2\n", (int) tb->controller_number, (tb->value ? "yes" : "no"))); + + ButtonID id (ButtonID (tb->controller_number)); + Button& button (get_button (id)); + + buttons_down.erase (id); + button.timeout_connection.disconnect (); + + ButtonState bs (ButtonState (0)); + + switch (id) { + case FaderTouch: + fader_is_touched = false; + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + framepos_t now = session->engine().sample_time(); + gain->stop_touch (true, now); + } + } + break; + default: + break; + } + + if (bs) { + button_state = ButtonState (button_state&~bs); + DEBUG_TRACE (DEBUG::CC121, string_compose ("reset button state to %1 using %2\n", button_state, (int) bs)); + } + + if (button.uses_flash()) { + button.set_led_state (_output_port, (int)tb->value); + } + + set::iterator c = consumed.find (id); + + if (c == consumed.end()) { + button.invoke (button_state, false); + } else { + DEBUG_TRACE (DEBUG::CC121, "button was consumed, ignored\n"); + consumed.erase (c); + } +} + +void +CC121::encoder_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb) +{ + DEBUG_TRACE (DEBUG::CC121, "encoder handler"); + + /* Extract absolute value*/ + float adj = static_cast(tb->value & ~0x40); + + /* Get direction (negative values start at 0x40)*/ + float sign = (tb->value & 0x40) ? -1.0 : 1.0; + switch(tb->controller_number) { + case 0x10: + /* pan */ + DEBUG_TRACE (DEBUG::CC121, "PAN encoder"); + if (_current_stripable) { + /* Get amount of change (encoder clicks) * (change per click)*/ + /*Create an exponential curve*/ + float curve = sign * pow(adj, (1.0 + 10.0) / 10.0); + adj = curve * (31 / 1000.0); + ardour_pan_azimuth (adj); + } + break; + case 0x20: + /* EQ 1 Q */ + break; + case 0x21: + /* EQ 2 Q */ + break; + case 0x22: + /* EQ 3 Q */ + break; + case 0x23: + /* EQ 4 Q */ + break; + case 0x30: + /* EQ 1 Frequency */ + break; + case 0x31: + /* EQ 2 Frequency */ + break; + case 0x32: + /* EQ 3 Frequency */ + break; + case 0x33: + /* EQ 4 Frequency */ + break; + case 0x3C: + /* AI */ + if (sign < 0.0f) { + if (_jogmode == scroll) { + ScrollTimeline(-0.05); + } + else { + ZoomIn(); + } + } + else { + if (_jogmode == scroll) { + ScrollTimeline(0.05); + } + else { + ZoomOut(); + } + } + break; + case 0x40: + /* EQ 1 Gain */ + break; + case 0x41: + /* EQ 2 Gain */ + break; + case 0x42: + /* EQ 3 Gain */ + break; + case 0x43: + /* EQ 4 Gain */ + break; + case 0x50: + /* Value */ + break; + default: + break; + } +} + +void +CC121::fader_handler (MIDI::Parser &, MIDI::pitchbend_t pb) +{ + DEBUG_TRACE (DEBUG::CC121, "fader handler"); + + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + float val = gain->interface_to_internal (pb/16384.0); + /* even though the cc121 only controls a + single stripable at a time, allow the fader to + modify the group, if appropriate. + */ + _current_stripable->gain_control()->set_value (val, Controllable::UseGroup); + } + } +} + +int +CC121::set_active (bool yn) +{ + DEBUG_TRACE (DEBUG::CC121, string_compose("CC121::set_active init with yn: '%1'\n", yn)); + + if (yn == active()) { + return 0; + } + + if (yn) { + + /* start event loop */ + + BaseUI::run (); + + connect_session_signals (); + + Glib::RefPtr blink_timeout = Glib::TimeoutSource::create (200); // milliseconds + blink_connection = blink_timeout->connect (sigc::mem_fun (*this, &CC121::blink)); + blink_timeout->attach (main_loop()->get_context()); + + Glib::RefPtr heartbeat_timeout = Glib::TimeoutSource::create (800); // milliseconds + heartbeat_connection = heartbeat_timeout->connect (sigc::mem_fun (*this, &CC121::beat)); + heartbeat_timeout->attach (main_loop()->get_context()); + + Glib::RefPtr periodic_timeout = Glib::TimeoutSource::create (100); // milliseconds + periodic_connection = periodic_timeout->connect (sigc::mem_fun (*this, &CC121::periodic)); + periodic_timeout->attach (main_loop()->get_context()); + + } else { + + BaseUI::quit (); + close (); + + } + + ControlProtocol::set_active (yn); + + DEBUG_TRACE (DEBUG::CC121, string_compose("CC121::set_active done with yn: '%1'\n", yn)); + + return 0; +} + +bool +CC121::periodic () +{ + if (!_current_stripable) { + return true; + } + + ARDOUR::AutoState gain_state = _current_stripable->gain_control()->automation_state(); + + if (gain_state == ARDOUR::Touch || gain_state == ARDOUR::Play) { + map_gain (); + } + + return true; +} + +void +CC121::stop_blinking (ButtonID id) +{ + blinkers.remove (id); + get_button (id).set_led_state (_output_port, false); +} + +void +CC121::start_blinking (ButtonID id) +{ + blinkers.push_back (id); + get_button (id).set_led_state (_output_port, true); +} + +bool +CC121::beat () +{ + MIDI::byte buf[8]; + + buf[0] = 0xf0; + buf[1] = 0x43; + buf[2] = 0x10; + buf[3] = 0x3e; + buf[4] = 0x15; + buf[5] = 0x00; + buf[6] = 0x01; + buf[7] = 0xF7; + + _output_port->write (buf, 8, 0); + + return true; +} + +bool +CC121::blink () +{ + blink_state = !blink_state; + + for (Blinkers::iterator b = blinkers.begin(); b != blinkers.end(); b++) { + get_button(*b).set_led_state (_output_port, blink_state); + } + + map_recenable_state (); + + return true; +} + +void +CC121::close () +{ + all_lights_out (); + + stop_midi_handling (); + session_connections.drop_connections (); + port_connection.disconnect (); + blink_connection.disconnect (); + heartbeat_connection.disconnect (); + selection_connection.disconnect (); + stripable_connections.drop_connections (); + +#if 0 + stripable_connections.drop_connections (); +#endif +} + +void +CC121::map_recenable_state () +{ + /* special case for RecEnable because its status can change as a + * confluence of unrelated parameters: (a) session rec-enable state (b) + * rec-enabled tracks. So we don't add the button to the blinkers list, + * we just call this: + * + * * from the blink callback + * * when the session tells us about a status change + * + * We do the last one so that the button changes state promptly rather + * than waiting for the next blink callback. The change in "blinking" + * based on having record-enabled tracks isn't urgent, and that happens + * during the blink callback. + */ + + bool onoff; + + switch (session->record_status()) { + case Session::Disabled: + onoff = false; + break; + case Session::Enabled: + onoff = blink_state; + break; + case Session::Recording: + if (session->have_rec_enabled_track ()) { + onoff = true; + } else { + onoff = blink_state; + } + break; + } + + if (onoff != rec_enable_state) { + get_button(RecEnable).set_led_state (_output_port, onoff); + rec_enable_state = onoff; + } +} + +void +CC121::map_transport_state () +{ + get_button (Loop).set_led_state (_output_port, session->get_play_loop()); + + float ts = session->transport_speed(); + + if (ts == 0) { + stop_blinking (Play); + } else if (fabs (ts) == 1.0) { + stop_blinking (Play); + get_button (Play).set_led_state (_output_port, true); + } else { + start_blinking (Play); + } + + get_button (Stop).set_led_state (_output_port, session->transport_stopped ()); + get_button (Rewind).set_led_state (_output_port, session->transport_speed() < 0.0); + get_button (Ffwd).set_led_state (_output_port, session->transport_speed() > 1.0); + get_button (Jog).set_led_state (_output_port, _jogmode == scroll); +} + +void +CC121::connect_session_signals() +{ + session->RecordStateChanged.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_recenable_state, this), this); + session->TransportStateChange.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_transport_state, this), this); +} + +bool +CC121::midi_input_handler (Glib::IOCondition ioc, boost::shared_ptr port) +{ + DEBUG_TRACE (DEBUG::CC121, string_compose ("something happend on %1\n", boost::shared_ptr(port)->name())); + + if (ioc & ~IO_IN) { + return false; + } + + if (ioc & IO_IN) { + + port->clear (); + DEBUG_TRACE (DEBUG::CC121, string_compose ("data available on %1\n", boost::shared_ptr(port)->name())); + framepos_t now = session->engine().sample_time(); + port->parse (now); + } + + return true; +} + + +XMLNode& +CC121::get_state () +{ + XMLNode& node (ControlProtocol::get_state()); + + XMLNode* child; + + child = new XMLNode (X_("Input")); + child->add_child_nocopy (boost::shared_ptr(_input_port)->get_state()); + node.add_child_nocopy (*child); + + + child = new XMLNode (X_("Output")); + child->add_child_nocopy (boost::shared_ptr(_output_port)->get_state()); + node.add_child_nocopy (*child); + + /* Save action state for Function1..4, Lock, Value, EQnEnable, EQType, + * AllBypass and Footswitch buttons, since these + * are user controlled. We can only save named-action operations, since + * internal functions are just pointers to functions and hard to + * serialize without enumerating them all somewhere. + */ + + node.add_child_nocopy (get_button (Function1).get_state()); + node.add_child_nocopy (get_button (Function2).get_state()); + node.add_child_nocopy (get_button (Function3).get_state()); + node.add_child_nocopy (get_button (Function4).get_state()); + node.add_child_nocopy (get_button (Value).get_state()); + node.add_child_nocopy (get_button (Lock).get_state()); + node.add_child_nocopy (get_button (EQ1Enable).get_state()); + node.add_child_nocopy (get_button (EQ2Enable).get_state()); + node.add_child_nocopy (get_button (EQ3Enable).get_state()); + node.add_child_nocopy (get_button (EQ4Enable).get_state()); + node.add_child_nocopy (get_button (EQType).get_state()); + node.add_child_nocopy (get_button (AllBypass).get_state()); + node.add_child_nocopy (get_button (Footswitch).get_state()); + + return node; +} + +int +CC121::set_state (const XMLNode& node, int version) +{ + 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) { + boost::shared_ptr(_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) { + boost::shared_ptr(_output_port)->set_state (*portnode, version); + } + } + + for (XMLNodeList::const_iterator n = node.children().begin(); n != node.children().end(); ++n) { + if ((*n)->name() == X_("Button")) { + XMLProperty const * prop = (*n)->property (X_("id")); + if (!prop) { + continue; + } + int xid = atoi (prop->value()); + ButtonMap::iterator b = buttons.find (ButtonID (xid)); + if (b == buttons.end()) { + continue; + } + b->second.set_state (**n); + } + } + + return 0; +} + +bool +CC121::connection_handler (boost::weak_ptr, std::string name1, boost::weak_ptr, std::string name2, bool yn) +{ + DEBUG_TRACE (DEBUG::CC121, "CC121::connection_handler start\n"); + if (!_input_port || !_output_port) { + return false; + } + + string ni = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (boost::shared_ptr(_input_port)->name()); + string no = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (boost::shared_ptr(_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 { + DEBUG_TRACE (DEBUG::CC121, string_compose ("Connections between %1 and %2 changed, but I ignored it\n", name1, name2)); + /* 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::CC121, "device now connected for both input and output\n"); + connected (); + + } else { + DEBUG_TRACE (DEBUG::CC121, "Device disconnected (input or output or both) or not yet fully connected\n"); + _device_active = false; + } + + ConnectionChange (); /* emit signal for our GUI */ + + DEBUG_TRACE (DEBUG::CC121, "CC121::connection_handler end\n"); + + return true; /* connection status changed */ +} + +void +CC121::connected () +{ + DEBUG_TRACE (DEBUG::CC121, "connected"); + + _device_active = true; + + start_midi_handling (); + + all_lights_out (); + + /* catch up on state */ + + /* make sure that rec_enable_state is consistent with current device state */ + get_button (RecEnable).set_led_state (_output_port, rec_enable_state); + + map_transport_state (); + map_recenable_state (); +} + +void +CC121::Button::invoke (CC121::ButtonState bs, bool press) +{ + DEBUG_TRACE (DEBUG::CC121, string_compose ("invoke button %1 for %2 state %3%4%5\n", id, (press ? "press":"release"), hex, bs, dec)); + + ToDoMap::iterator x; + + if (press) { + if ((x = on_press.find (bs)) == on_press.end()) { + DEBUG_TRACE (DEBUG::CC121, string_compose ("no press action for button %1 state %2 @ %3 in %4\n", id, bs, this, &on_press)); + return; + } + } else { + if ((x = on_release.find (bs)) == on_release.end()) { + DEBUG_TRACE (DEBUG::CC121, string_compose ("no release action for button %1 state %2 @%3 in %4\n", id, bs, this, &on_release)); + return; + } + } + + switch (x->second.type) { + case NamedAction: + if (!x->second.action_name.empty()) { + fp.access_action (x->second.action_name); + } + break; + case InternalFunction: + if (x->second.function) { + x->second.function (); + } + } +} + +void +CC121::Button::set_action (string const& name, bool when_pressed, CC121::ButtonState bs) +{ + ToDo todo; + + todo.type = NamedAction; + + if (when_pressed) { + if (name.empty()) { + on_press.erase (bs); + } else { + DEBUG_TRACE (DEBUG::CC121, string_compose ("set button %1 to action %2 on press + %3%4%5\n", id, name, bs)); + todo.action_name = name; + on_press[bs] = todo; + } + } else { + if (name.empty()) { + on_release.erase (bs); + } else { + DEBUG_TRACE (DEBUG::CC121, string_compose ("set button %1 to action %2 on release + %3%4%5\n", id, name, bs)); + todo.action_name = name; + on_release[bs] = todo; + } + } +} + +string +CC121::Button::get_action (bool press, CC121::ButtonState bs) +{ + ToDoMap::iterator x; + + if (press) { + if ((x = on_press.find (bs)) == on_press.end()) { + return string(); + } + if (x->second.type != NamedAction) { + return string (); + } + return x->second.action_name; + } else { + if ((x = on_release.find (bs)) == on_release.end()) { + return string(); + } + if (x->second.type != NamedAction) { + return string (); + } + return x->second.action_name; + } +} + +void +CC121::Button::set_action (boost::function f, bool when_pressed, CC121::ButtonState bs) +{ + ToDo todo; + todo.type = InternalFunction; + + if (when_pressed) { + DEBUG_TRACE (DEBUG::CC121, string_compose ("set button %1 (%2) @ %5 to some functor on press + %3 in %4\n", id, name, bs, &on_press, this)); + todo.function = f; + on_press[bs] = todo; + } else { + DEBUG_TRACE (DEBUG::CC121, string_compose ("set button %1 (%2) @ %5 to some functor on release + %3\n", id, name, bs, this)); + todo.function = f; + on_release[bs] = todo; + } +} + +void +CC121::Button::set_led_state (boost::shared_ptr port, bool onoff) +{ + MIDI::byte buf[3]; + DEBUG_TRACE(DEBUG::CC121, "Set Led State\n"); + buf[0] = 0x90; + buf[1] = id; + buf[2] = (onoff ? 0x7F:0x00); + port->write (buf, 3, 0); +} + +int +CC121::Button::set_state (XMLNode const& node) +{ + const XMLProperty* prop = node.property ("id"); + if (!prop) { + return -1; + } + + int xid = atoi (prop->value()); + if (xid != id) { + return -1; + } + + typedef pair state_pair_t; + vector state_pairs; + + state_pairs.push_back (make_pair (string ("plain"), ButtonState (0))); + + for (vector::const_iterator sp = state_pairs.begin(); sp != state_pairs.end(); ++sp) { + string propname; + + propname = sp->first + X_("-press"); + if ((prop = node.property (propname)) != 0) { + set_action (prop->value(), true, sp->second); + } + + propname = sp->first + X_("-release"); + if ((prop = node.property (propname)) != 0) { + set_action (prop->value(), false, sp->second); + } + } + + return 0; +} + +XMLNode& +CC121::Button::get_state () const +{ + XMLNode* node = new XMLNode (X_("Button")); + char buf[16]; + snprintf (buf, sizeof (buf), "%d", id); + + node->add_property (X_("id"), buf); + + ToDoMap::const_iterator x; + ToDo null; + null.type = NamedAction; + + typedef pair state_pair_t; + vector state_pairs; + + state_pairs.push_back (make_pair (string ("plain"), ButtonState (0))); + + for (vector::const_iterator sp = state_pairs.begin(); sp != state_pairs.end(); ++sp) { + if ((x = on_press.find (sp->second)) != on_press.end()) { + if (x->second.type == NamedAction) { + node->add_property (string (sp->first + X_("-press")).c_str(), x->second.action_name); + } + } + + if ((x = on_release.find (sp->second)) != on_release.end()) { + if (x->second.type == NamedAction) { + node->add_property (string (sp->first + X_("-release")).c_str(), x->second.action_name); + } + } + } + + return *node; +} + +void +CC121::gui_track_selection_changed (StripableNotificationListPtr stripables) +{ + boost::shared_ptr r; + + if (!stripables->empty()) { + r = stripables->front().lock(); + } + + set_current_stripable (r); +} + +void +CC121::drop_current_stripable () +{ + if (_current_stripable) { + if (_current_stripable == session->monitor_out()) { + set_current_stripable (session->master_out()); + } else { + set_current_stripable (boost::shared_ptr()); + } + } +} + +void +CC121::set_current_stripable (boost::shared_ptr r) +{ + stripable_connections.drop_connections (); + + _current_stripable = r; + + if (_current_stripable) { + _current_stripable->DropReferences.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::drop_current_stripable, this), this); + + _current_stripable->mute_control()->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_mute, this), this); + _current_stripable->solo_control()->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_solo, this), this); + + boost::shared_ptr t = boost::dynamic_pointer_cast (_current_stripable); + if (t) { + t->rec_enable_control()->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_recenable, this), this); + } + + boost::shared_ptr control = _current_stripable->gain_control (); + if (control) { + control->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_gain, this), this); + control->alist()->automation_state_changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_auto, this), this); + } + + boost::shared_ptr mp = _current_stripable->monitor_control(); + if (mp) { + mp->cut_control()->Changed.connect (stripable_connections, MISSING_INVALIDATOR, boost::bind (&CC121::map_cut, this), this); + } + } + + //ToDo: subscribe to the fader automation modes so we can light the LEDs + + map_stripable_state (); +} + +void +CC121::map_auto () +{ + boost::shared_ptr control = _current_stripable->gain_control (); + const AutoState as = control->automation_state (); + + switch (as) { + case ARDOUR::Play: + get_button (FP_Read).set_led_state (_output_port, true); + get_button (FP_Write).set_led_state (_output_port, false); + get_button (EButton).set_led_state (_output_port, false); + get_button (OpenVST).set_led_state (_output_port, false); + break; + case ARDOUR::Write: + get_button (FP_Read).set_led_state (_output_port, false); + get_button (FP_Write).set_led_state (_output_port, true); + get_button (EButton).set_led_state (_output_port, false); + get_button (OpenVST).set_led_state (_output_port, false); + break; + case ARDOUR::Touch: + get_button (EButton).set_led_state (_output_port, true); + get_button (FP_Read).set_led_state (_output_port, false); + get_button (FP_Write).set_led_state(_output_port, false); + get_button (OpenVST).set_led_state (_output_port, false); + break; + case ARDOUR::Off: + get_button (OpenVST).set_led_state (_output_port, true); + get_button (FP_Read).set_led_state (_output_port, false); + get_button (FP_Write).set_led_state (_output_port, false); + get_button (EButton).set_led_state (_output_port, false); + break; + } +} + +void +CC121::map_cut () +{ + boost::shared_ptr mp = _current_stripable->monitor_control(); + + if (mp) { + bool yn = mp->cut_all (); + if (yn) { + start_blinking (Mute); + } else { + stop_blinking (Mute); + } + } else { + stop_blinking (Mute); + } +} + +void +CC121::map_mute () +{ + if (_current_stripable) { + if (_current_stripable->mute_control()->muted()) { + stop_blinking (Mute); + get_button (Mute).set_led_state (_output_port, true); + } else if (_current_stripable->mute_control()->muted_by_others_soloing () || _current_stripable->mute_control()->muted_by_masters()) { + start_blinking (Mute); + } else { + stop_blinking (Mute); + } + } else { + stop_blinking (Mute); + } +} + +void +CC121::map_solo () +{ + if (_current_stripable) { + get_button (Solo).set_led_state (_output_port, _current_stripable->solo_control()->soloed()); + } else { + get_button (Solo).set_led_state (_output_port, false); + } +} + +void +CC121::map_recenable () +{ + boost::shared_ptr t = boost::dynamic_pointer_cast (_current_stripable); + if (t) { + get_button (Rec).set_led_state (_output_port, t->rec_enable_control()->get_value()); + } else { + get_button (Rec).set_led_state (_output_port, false); + } +} + +void +CC121::map_gain () +{ + if (fader_is_touched) { + /* Do not send fader moves while the user is touching the fader */ + return; + } + + if (!_current_stripable) { + return; + } + + boost::shared_ptr control = _current_stripable->gain_control (); + double val; + + if (!control) { + val = 0.0; + } else { + val = control->internal_to_interface (control->get_value ()); + } + + float fval = (val* 16384.0); + if (fval <0.0) + fval = 0.0; + else if (fval > 16383.0) + fval = 16383.0; + int ival = (int)(fval + 0.5); + + MIDI::byte buf[3]; + + buf[0] = 0xE0; + buf[1] = ival & 0x7F; + buf[2] = (ival >> 7) & 0x7F; + + _output_port->write (buf, 3, 0); +} + +void +CC121::map_stripable_state () +{ + if (!_current_stripable) { + stop_blinking (Mute); + stop_blinking (Solo); + get_button (Rec).set_led_state (_output_port, false); + } else { + map_solo (); + map_recenable (); + map_gain (); + map_auto (); + + if (_current_stripable == session->monitor_out()) { + map_cut (); + } else { + map_mute (); + } + } +} + +list > +CC121::bundles () +{ + list > b; + + if (_input_bundle) { + b.push_back (_input_bundle); + b.push_back (_output_bundle); + } + + return b; +} + +boost::shared_ptr +CC121::output_port() +{ + return _output_port; +} + +boost::shared_ptr +CC121::input_port() +{ + return _input_port; +} + +void +CC121::set_action (ButtonID id, std::string const& action_name, bool on_press, ButtonState bs) +{ + get_button(id).set_action (action_name, on_press, bs); +} + +string +CC121::get_action (ButtonID id, bool press, ButtonState bs) +{ + return get_button(id).get_action (press, bs); +} diff --git a/libs/surfaces/cc121/cc121.h b/libs/surfaces/cc121/cc121.h new file mode 100644 index 0000000000..a5f0363a22 --- /dev/null +++ b/libs/surfaces/cc121/cc121.h @@ -0,0 +1,342 @@ +/* + Copyright (C) 2006 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + Thanks to Rolf Meyerhoff for reverse engineering the CC121 protocol. + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef ardour_surface_cc121_h +#define ardour_surface_cc121_h + +#include +#include +#include +#include + +#define ABSTRACT_UI_EXPORTS +#include "pbd/abstract_ui.h" + +#include "ardour/types.h" + +#include "control_protocol/control_protocol.h" + +namespace PBD { + class Controllable; + class ControllableDescriptor; +} + +#include + +//#include "pbd/signals.h" + + +//#include "midi_byte_array.h" +#include "types.h" + +#include "glibmm/main.h" + +namespace MIDI { + class Parser; + class Port; +} + + +namespace ARDOUR { + class AsyncMIDIPort; + class Bundle; + class Port; + class Session; + class MidiPort; +} + + +class MIDIControllable; +class MIDIFunction; +class MIDIAction; + +namespace ArdourSurface { + +struct CC121Request : public BaseUI::BaseRequestObject { +public: + CC121Request () {} + ~CC121Request () {} +}; + +class CC121 : public ARDOUR::ControlProtocol, public AbstractUI { + public: + CC121 (ARDOUR::Session&); + virtual ~CC121(); + + 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); + + bool has_editor () const { return true; } + void* get_gui () const; + void tear_down_gui (); + + + /* Note: because the CC121 speaks an inherently duplex protocol, + we do not implement get/set_feedback() since this aspect of + support for the protocol is not optional. + */ + + void do_request (CC121Request*); + int stop (); + + void thread_init (); + + PBD::Signal0 ConnectionChange; + + boost::shared_ptr input_port(); + boost::shared_ptr output_port(); + + enum ButtonID { + Rec = 0x00, + Solo = 0x08, + Mute = 0x10, + Left = 0x30, + Right = 0x31, + EButton = 0x33, + Function1 = 0x36, + Function2 = 0x37, + Function3 = 0x38, + Function4 = 0x39, + Value = 0x3A, + Footswitch = 0x3B, + FP_Read = 0x4A, + FP_Write = 0x4B, + Loop = 0x56, + ToStart = 0x58, + ToEnd = 0x5A, + Rewind = 0x5B, + Ffwd = 0x5C, + Stop = 0x5D, + Play = 0x5E, + RecEnable = 0x5F, + FaderTouch = 0x68, + EQ1Enable = 0x70, + EQ2Enable = 0x71, + EQ3Enable = 0x72, + EQ4Enable = 0x73, + EQType = 0x74, + AllBypass = 0x75, + Jog = 0x76, + Lock = 0x77, + InputMonitor = 0x78, + OpenVST = 0x79, + Output = 22 + }; + + enum ButtonState { + ShiftDown = 0x1, + RewindDown = 0x2, + StopDown = 0x4, + UserDown = 0x8, + LongPress = 0x10 + }; + + void set_action (ButtonID, std::string const& action_name, bool on_press, CC121::ButtonState = ButtonState (0)); + std::string get_action (ButtonID, bool on_press, CC121::ButtonState = ButtonState (0)); + + std::list > bundles (); + + private: + boost::shared_ptr _current_stripable; + boost::weak_ptr pre_master_stripable; + boost::weak_ptr pre_monitor_stripable; + + boost::shared_ptr _input_port; + boost::shared_ptr _output_port; + + // Bundle to represent our input ports + boost::shared_ptr _input_bundle; + // Bundle to represent our output ports + boost::shared_ptr _output_bundle; + + PBD::ScopedConnectionList midi_connections; + + bool midi_input_handler (Glib::IOCondition ioc, boost::shared_ptr port); + + mutable void *gui; + void build_gui (); + + bool connection_handler (boost::weak_ptr, std::string name1, boost::weak_ptr, std::string name2, bool yn); + PBD::ScopedConnection port_connection; + + enum ConnectionState { + InputConnected = 0x1, + OutputConnected = 0x2 + }; + + int connection_state; + void connected (); + bool _device_active; + int fader_msb; + int fader_lsb; + bool fader_is_touched; + enum JogMode { scroll=1, zoom=2 }; + JogMode _jogmode; + + ARDOUR::microseconds_t last_encoder_time; + int last_good_encoder_delta; + int last_encoder_delta, last_last_encoder_delta; + + void button_press_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb); + void button_release_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb); + void fader_handler (MIDI::Parser &, MIDI::pitchbend_t pb); + void encoder_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb); + /* void fader_handler (MIDI::Parser &, MIDI::EventTwoBytes* tb);*/ + + ButtonState button_state; + + friend class Button; + + class Button { + public: + + enum ActionType { + NamedAction, + InternalFunction, + }; + + Button (CC121& f, std::string const& str, ButtonID i) + : fp (f) + , name (str) + , id (i) + , flash (false) + {} + + void set_action (std::string const& action_name, bool on_press, CC121::ButtonState = ButtonState (0)); + void set_action (boost::function function, bool on_press, CC121::ButtonState = ButtonState (0)); + std::string get_action (bool press, CC121::ButtonState bs = ButtonState (0)); + + void set_led_state (boost::shared_ptr, bool onoff); + void invoke (ButtonState bs, bool press); + bool uses_flash () const { return flash; } + void set_flash (bool yn) { flash = yn; } + + XMLNode& get_state () const; + int set_state (XMLNode const&); + + sigc::connection timeout_connection; + + private: + CC121& fp; + std::string name; + ButtonID id; + bool flash; + + struct ToDo { + ActionType type; + /* could be a union if boost::function didn't require a + * constructor + */ + std::string action_name; + boost::function function; + }; + + typedef std::map ToDoMap; + ToDoMap on_press; + ToDoMap on_release; + }; + + typedef std::map ButtonMap; + + ButtonMap buttons; + Button& get_button (ButtonID) const; + + std::set buttons_down; + std::set consumed; + + void all_lights_out (); + void close (); + void start_midi_handling (); + void stop_midi_handling (); + + PBD::ScopedConnectionList session_connections; + void connect_session_signals (); + void map_recenable_state (); + void map_transport_state (); + + sigc::connection periodic_connection; + bool periodic (); + + sigc::connection heartbeat_connection; + sigc::connection blink_connection; + typedef std::list Blinkers; + Blinkers blinkers; + bool blink_state; + bool blink (); + bool beat (); + void start_blinking (ButtonID); + void stop_blinking (ButtonID); + + void set_current_stripable (boost::shared_ptr); + void drop_current_stripable (); + void use_master (); + void use_monitor (); + void gui_track_selection_changed (ARDOUR::StripableNotificationListPtr); + PBD::ScopedConnection selection_connection; + PBD::ScopedConnectionList stripable_connections; + + void map_stripable_state (); + void map_solo (); + void map_mute (); + bool rec_enable_state; + void map_recenable (); + void map_gain (); + void map_cut (); + void map_auto (); + + /* operations (defined in operations.cc) */ + + void read (); + void write (); + + void input_monitor (); + void left (); + void right (); + + void touch (); + void off (); + + void undo (); + void redo (); + void solo (); + void mute (); + void jog (); + void rec_enable (); + + void ardour_pan_azimuth (float); + void ardour_pan_width (float); + void mixbus_pan (float); + + void punch (); +}; + +} + +#endif /* ardour_surface_cc121_h */ diff --git a/libs/surfaces/cc121/cc121_interface.cc b/libs/surfaces/cc121/cc121_interface.cc new file mode 100644 index 0000000000..7483b39d35 --- /dev/null +++ b/libs/surfaces/cc121/cc121_interface.cc @@ -0,0 +1,80 @@ +/* + Copyright (C) 2012 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include + +#include "control_protocol/control_protocol.h" +#include "cc121.h" + +using namespace ARDOUR; +using namespace ArdourSurface; + +static ControlProtocol* +new_cc121_midi_protocol (ControlProtocolDescriptor* /*descriptor*/, Session* s) +{ + CC121* fp; + + try { + fp = new CC121 (*s); + } catch (failed_constructor& err) { + return 0; + } + + if (fp->set_active (true)) { + delete fp; + return 0; + } + + return fp; +} + +static void +delete_cc121_midi_protocol (ControlProtocolDescriptor* /*descriptor*/, ControlProtocol* cp) +{ + delete cp; +} + +static bool +probe_cc121_midi_protocol (ControlProtocolDescriptor* /*descriptor*/) +{ + return CC121::probe (); +} + +static void* +cc121_request_buffer_factory (uint32_t num_requests) +{ + return CC121::request_factory (num_requests); +} + +static ControlProtocolDescriptor cc121_midi_descriptor = { + /*name : */ "Steinberg CC121", + /*id : */ "uri://ardour.org/surfaces/cc121:0", + /*ptr : */ 0, + /*module : */ 0, + /*mandatory : */ 0, + /*supports_feedback : */ true, + /*probe : */ probe_cc121_midi_protocol, + /*initialize : */ new_cc121_midi_protocol, + /*destroy : */ delete_cc121_midi_protocol, + /*request_buffer_factory */ cc121_request_buffer_factory +}; + +extern "C" ARDOURSURFACE_API ControlProtocolDescriptor* protocol_descriptor () { return &cc121_midi_descriptor; } + diff --git a/libs/surfaces/cc121/gui.cc b/libs/surfaces/cc121/gui.cc new file mode 100644 index 0000000000..ca06887102 --- /dev/null +++ b/libs/surfaces/cc121/gui.cc @@ -0,0 +1,629 @@ +/* + Copyright (C) 2015 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include +#include +#include + +#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 "cc121.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* +CC121::get_gui () const +{ + if (!gui) { + const_cast(this)->build_gui (); + } + static_cast(gui)->show_all(); + return gui; +} + +void +CC121::tear_down_gui () +{ + if (gui) { + Gtk::Widget *w = static_cast(gui)->get_parent(); + if (w) { + w->hide(); + delete w; + } + } + delete static_cast (gui); + gui = 0; +} + +void +CC121::build_gui () +{ + gui = (void*) new CC121GUI (*this); +} + +/*--------------------*/ + +CC121GUI::CC121GUI (CC121& p) + : fp (p) + , table (2, 5) + , action_table (5, 4) + , 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); + + std::string data_file_path; + string name = "cc121.png"; + Searchpath spath(ARDOUR::ardour_data_search_path()); + spath.add_subdirectory_to_paths ("icons"); + find_file (spath, name, data_file_path); + if (!data_file_path.empty()) { + image.set (data_file_path); + hpacker.pack_start (image, false, false); + } + + Gtk::Label* l; + Gtk::Alignment* align; + int row = 0; + int action_row = 1; + + 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, &CC121GUI::active_port_changed), &input_combo, true)); + output_combo.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &CC121GUI::active_port_changed), &output_combo, false)); + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Incoming MIDI on:"))); + l->set_alignment (1.0, 0.5); + table.attach (*l, 0, 1, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0)); + table.attach (input_combo, 1, 2, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0), 0, 0); + row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Outgoing MIDI on:"))); + l->set_alignment (1.0, 0.5); + table.attach (*l, 0, 1, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0)); + table.attach (output_combo, 1, 2, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions(0), 0, 0); + row++; + + build_available_action_menu (); + + build_user_action_combo (function1_combo, CC121::ButtonState(0), CC121::Function1); + build_user_action_combo (function2_combo, CC121::ButtonState(0), CC121::Function2); + build_user_action_combo (function3_combo, CC121::ButtonState(0), CC121::Function3); + build_user_action_combo (function4_combo, CC121::ButtonState(0), CC121::Function4); + build_user_action_combo (value_combo, CC121::ButtonState(0), CC121::Value); + build_user_action_combo (lock_combo, CC121::ButtonState(0), CC121::Lock); + build_user_action_combo (eq1_combo, CC121::ButtonState(0), CC121::EQ1Enable); + build_user_action_combo (eq2_combo, CC121::ButtonState(0), CC121::EQ2Enable); + build_user_action_combo (eq3_combo, CC121::ButtonState(0), CC121::EQ3Enable); + build_user_action_combo (eq4_combo, CC121::ButtonState(0), CC121::EQ4Enable); + build_user_action_combo (eqtype_combo, CC121::ButtonState(0), CC121::EQType); + build_user_action_combo (allbypass_combo, CC121::ButtonState(0), CC121::AllBypass); + build_foot_action_combo (foot_combo, CC121::ButtonState(0)); + action_table.set_row_spacings (4); + action_table.set_col_spacings (6); + action_table.set_border_width (12); + action_table.set_homogeneous (false); + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Function 1"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (function1_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Function 2"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (function2_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Function 3"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (function3_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Function 4"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (function4_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Value"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (value_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Lock"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (lock_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("EQ1"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (eq1_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("EQ2"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (eq2_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("EQ3"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (eq3_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("EQ4"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (eq4_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("EQType"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (eqtype_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("AllBypass"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (allbypass_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + l = manage (new Gtk::Label); + l->set_markup (string_compose ("%1", _("Footswitch"))); + l->set_alignment (1.0, 0.5); + action_table.attach (*l, 0, 1, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + align = manage (new Alignment); + align->set (0.0, 0.5); + align->add (foot_combo); + action_table.attach (*align, 1, 2, action_row, action_row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + action_row++; + + table.attach (action_table, 0, 5, row, row+1, AttachOptions(FILL|EXPAND), AttachOptions (0)); + row++; + + hpacker.pack_start (table, true, true); + pack_start (hpacker, false, false); + + /* update the port connection combos */ + + update_port_combos (); + + /* catch future changes to connection state */ + + fp.ConnectionChange.connect (connection_change_connection, invalidator (*this), boost::bind (&CC121GUI::connection_handler, this), gui_context()); +} + +CC121GUI::~CC121GUI () +{ +} + +void +CC121GUI::connection_handler () +{ + /* ignore all changes to combobox active strings here, because we're + updating them to match a new ("external") reality - we were called + because port connections have changed. + */ + + PBD::Unwinder ici (ignore_active_change, true); + + update_port_combos (); +} + +void +CC121GUI::update_port_combos () +{ + vector midi_inputs; + vector 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 input = build_midi_port_list (midi_inputs, true); + Glib::RefPtr 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 */ + } +} + +void +CC121GUI::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 paths; + vector labels; + vector tooltips; + vector keys; + vector > actions; + + Gtkmm2ext::ActionMap::get_all_actions (paths, labels, tooltips, keys, actions); + + typedef std::map NodeMap; + NodeMap nodes; + NodeMap::iterator r; + + + vector::iterator k; + vector::iterator p; + vector::iterator t; + vector::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 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 "/" prefix part of a path. + */ + path = path.substr (strlen ("/")); + + row[action_columns.path] = path; + } +} + +void +CC121GUI::action_changed (Gtk::ComboBox* cb, CC121::ButtonID id, CC121::ButtonState bs) +{ + TreeModel::const_iterator row = cb->get_active (); + string action_path = (*row)[action_columns.path]; + + /* release binding */ + fp.set_action (id, action_path, false, bs); +} + +void +CC121GUI::build_action_combo (Gtk::ComboBox& cb, vector > const & actions, CC121::ButtonID id, CC121::ButtonState bs) +{ + Glib::RefPtr model (Gtk::ListStore::create (action_columns)); + TreeIter rowp; + TreeModel::Row row; + string current_action = fp.get_action (id, false, bs); /* lookup release action */ + int active_row = -1; + int n; + vector >::const_iterator i; + + rowp = model->append(); + row = *(rowp); + row[action_columns.name] = _("Disabled"); + row[action_columns.path] = string(); + + if (current_action.empty()) { + active_row = 0; + } + + for (i = actions.begin(), n = 0; i != actions.end(); ++i, ++n) { + rowp = model->append(); + row = *(rowp); + row[action_columns.name] = i->first; + row[action_columns.path] = i->second; + if (current_action == i->second) { + active_row = n+1; + } + } + + cb.set_model (model); + cb.pack_start (action_columns.name); + + if (active_row >= 0) { + cb.set_active (active_row); + } + + cb.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &CC121GUI::action_changed), &cb, id, bs)); +} + +void +CC121GUI::build_foot_action_combo (Gtk::ComboBox& cb, CC121::ButtonState bs) +{ + vector > actions; + + actions.push_back (make_pair (string("Toggle Roll"), string(X_("Transport/ToggleRoll")))); + actions.push_back (make_pair (string("Toggle Rec-Enable"), string(X_("Transport/Record")))); + actions.push_back (make_pair (string("Toggle Roll+Rec"), string(X_("Transport/record-roll")))); + actions.push_back (make_pair (string("Toggle Loop"), string(X_("Transport/Loop")))); + actions.push_back (make_pair (string("Toggle Click"), string(X_("Transport/ToggleClick")))); + + build_action_combo (cb, actions, CC121::Footswitch, bs); +} + +bool +CC121GUI::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 +CC121GUI::build_user_action_combo (Gtk::ComboBox& cb, CC121::ButtonState bs, CC121::ButtonID id) +{ + cb.set_model (available_action_model); + cb.pack_start (action_columns.name); + cb.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &CC121GUI::action_changed), &cb, id, bs)); + + /* set the active "row" to the right value for the current button binding */ + + string current_action = fp.get_action (id, false, bs); /* lookup release action */ + + if (current_action.empty()) { + cb.set_active (0); /* "disabled" */ + return; + } + + TreeModel::iterator iter = available_action_model->children().end(); + + available_action_model->foreach_iter (sigc::bind (sigc::mem_fun (*this, &CC121GUI::find_action_in_model), current_action, &iter)); + + if (iter != available_action_model->children().end()) { + cb.set_active (iter); + } else { + cb.set_active (0); + } +} + +Glib::RefPtr +CC121GUI::build_midi_port_list (vector const & ports, bool for_input) +{ + Glib::RefPtr 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::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 +CC121GUI::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); + } + } +} diff --git a/libs/surfaces/cc121/gui.h b/libs/surfaces/cc121/gui.h new file mode 100644 index 0000000000..107a81ed32 --- /dev/null +++ b/libs/surfaces/cc121/gui.h @@ -0,0 +1,116 @@ +/* + Copyright (C) 2015 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef __ardour_cc121_gui_h__ +#define __ardour_cc121_gui_h__ + +#include +#include + +#include +#include +#include +#include +#include + +namespace Gtk { + class CellRendererCombo; + class ListStore; +} + +#include "cc121.h" + +namespace ArdourSurface { + +class CC121GUI : public Gtk::VBox +{ +public: + CC121GUI (CC121&); + ~CC121GUI (); + +private: + CC121& fp; + Gtk::HBox hpacker; + Gtk::Table table; + Gtk::Table action_table; + Gtk::ComboBox input_combo; + Gtk::ComboBox output_combo; + Gtk::Image image; + + Gtk::ComboBox foot_combo; + Gtk::ComboBox function1_combo; + Gtk::ComboBox function2_combo; + Gtk::ComboBox function3_combo; + Gtk::ComboBox function4_combo; + Gtk::ComboBox value_combo; + Gtk::ComboBox lock_combo; + Gtk::ComboBox eq1_combo; + Gtk::ComboBox eq2_combo; + Gtk::ComboBox eq3_combo; + Gtk::ComboBox eq4_combo; + Gtk::ComboBox eqtype_combo; + Gtk::ComboBox allbypass_combo; + + void update_port_combos (); + PBD::ScopedConnection connection_change_connection; + void connection_handler (); + + struct MidiPortColumns : public Gtk::TreeModel::ColumnRecord { + MidiPortColumns() { + add (short_name); + add (full_name); + } + Gtk::TreeModelColumn short_name; + Gtk::TreeModelColumn full_name; + }; + + MidiPortColumns midi_port_columns; + bool ignore_active_change; + + Glib::RefPtr build_midi_port_list (std::vector const & ports, bool for_input); + void active_port_changed (Gtk::ComboBox*,bool for_input); + + struct ActionColumns : public Gtk::TreeModel::ColumnRecord { + ActionColumns() { + add (name); + add (path); + } + Gtk::TreeModelColumn name; + Gtk::TreeModelColumn path; + }; + + ActionColumns action_columns; + Glib::RefPtr available_action_model; + std::map action_map; // map from action names to paths + + void build_action_combo (Gtk::ComboBox& cb, std::vector > const & actions, CC121::ButtonID, CC121::ButtonState); + void build_user_action_combo (Gtk::ComboBox&, CC121::ButtonState, CC121::ButtonID); + void build_foot_action_combo (Gtk::ComboBox&, CC121::ButtonState); + + void build_available_action_menu (); + void action_changed (Gtk::ComboBox*, CC121::ButtonID, CC121::ButtonState); + + bool find_action_in_model (const Gtk::TreeModel::iterator& iter, std::string const & action_path, Gtk::TreeModel::iterator* found); + +}; + +} + +#endif /* __ardour_cc121_gui_h__ */ diff --git a/libs/surfaces/cc121/operations.cc b/libs/surfaces/cc121/operations.cc new file mode 100644 index 0000000000..67aafda777 --- /dev/null +++ b/libs/surfaces/cc121/operations.cc @@ -0,0 +1,340 @@ +/* + Copyright (C) 2015 Paul Davis + Copyright (C) 2016 W.P. van Paassen + + Thanks to Rolf Meyerhoff for reverse engineering the CC121 protocol. + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include "ardour/async_midi_port.h" +#include "ardour/monitor_processor.h" +#include "ardour/monitor_control.h" +#include "ardour/pannable.h" +#include "ardour/plugin_insert.h" +#include "ardour/rc_configuration.h" +#include "ardour/record_enable_control.h" +#include "ardour/session.h" +#include "ardour/track.h" +#include "ardour/types.h" + +#include "cc121.h" + +using namespace ARDOUR; +using namespace ArdourSurface; +using namespace PBD; + +/* this value is chosen to given smooth motion from 0..1.0 in about 270 degrees + * of encoder rotation. + */ +static const double encoder_divider = 24.0; + +void +CC121::input_monitor () +{ + if (_current_stripable) { + MonitorChoice choice = _current_stripable->monitoring_control()->monitoring_choice (); + switch(choice) { + case MonitorAuto: + _current_stripable->monitoring_control()->set_value (MonitorInput, PBD::Controllable::NoGroup); + get_button(InputMonitor).set_led_state (_output_port, true); + break; + case MonitorInput: + _current_stripable->monitoring_control()->set_value (MonitorDisk, PBD::Controllable::NoGroup); + get_button(InputMonitor).set_led_state (_output_port, false); + break; + case MonitorDisk: + _current_stripable->monitoring_control()->set_value (MonitorCue, PBD::Controllable::NoGroup); + get_button(InputMonitor).set_led_state (_output_port, false); + break; + case MonitorCue: + _current_stripable->monitoring_control()->set_value (MonitorInput, PBD::Controllable::NoGroup); + get_button(InputMonitor).set_led_state (_output_port, true); + break; + default: + break; + } + } +} + +void +CC121::left () +{ + access_action ("Editor/select-prev-route"); + + //ToDo: bank by 8? + //if ( (button_state & ShiftDown) == ShiftDown ) + +} + +void +CC121::right () +{ + access_action ("Editor/select-next-route"); + + //ToDo: bank by 8? + //if ( (button_state & ShiftDown) == ShiftDown ) +} + + +void +CC121::read () +{ + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + gain->set_automation_state( (ARDOUR::AutoState) ARDOUR::Play ); + } + } +} + +void +CC121::write () +{ + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + gain->set_automation_state( (ARDOUR::AutoState) ARDOUR::Write ); + } + } +} + +void +CC121::touch () +{ + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + gain->set_automation_state( (ARDOUR::AutoState) ARDOUR::Touch ); + } + } +} + +void +CC121::off () +{ + if (_current_stripable) { + boost::shared_ptr gain = _current_stripable->gain_control (); + if (gain) { + gain->set_automation_state( (ARDOUR::AutoState) ARDOUR::Off ); + } + } +} + + + + +void +CC121::undo () +{ + ControlProtocol::Undo (); /* EMIT SIGNAL */ +} + +void +CC121::redo () +{ + ControlProtocol::Redo (); /* EMIT SIGNAL */ +} + +void +CC121::jog() +{ + if (_jogmode == scroll) { + _jogmode = zoom; + } + else { + _jogmode = scroll; + } + get_button (Jog).set_led_state (_output_port, _jogmode == scroll); +} + +void +CC121::mute () +{ + if (!_current_stripable) { + return; + } + + if (_current_stripable == session->monitor_out()) { + boost::shared_ptr mp = _current_stripable->monitor_control(); + mp->set_cut_all (!mp->cut_all()); + return; + } + + _current_stripable->mute_control()->set_value (!_current_stripable->mute_control()->muted(), PBD::Controllable::UseGroup); +} + +void +CC121::solo () +{ + if (!_current_stripable) { + return; + } + _current_stripable->solo_control()->set_value (!_current_stripable->solo_control()->soloed(), PBD::Controllable::UseGroup); +} + +void +CC121::rec_enable () +{ + if (!_current_stripable) { + return; + } + + boost::shared_ptr t = boost::dynamic_pointer_cast(_current_stripable); + + if (!t) { + return; + } + + t->rec_enable_control()->set_value (!t->rec_enable_control()->get_value(), Controllable::UseGroup); +} + +void +CC121::use_master () +{ + boost::shared_ptr r = session->master_out(); + if (r) { + if (_current_stripable == r) { + r = pre_master_stripable.lock(); + set_current_stripable (r); + get_button(Output).set_led_state (_output_port, false); + blinkers.remove (Output); + } else { + if (_current_stripable != session->master_out() && _current_stripable != session->monitor_out()) { + pre_master_stripable = boost::weak_ptr (_current_stripable); + } + set_current_stripable (r); + get_button(Output).set_led_state (_output_port, true); + blinkers.remove (Output); + } + } +} + +void +CC121::use_monitor () +{ + boost::shared_ptr r = session->monitor_out(); + + if (r) { + if (_current_stripable == r) { + r = pre_monitor_stripable.lock(); + set_current_stripable (r); + get_button(Output).set_led_state (_output_port, false); + blinkers.remove (Output); + } else { + if (_current_stripable != session->master_out() && _current_stripable != session->monitor_out()) { + pre_monitor_stripable = boost::weak_ptr (_current_stripable); + } + set_current_stripable (r); + get_button(Output).set_led_state (_output_port, true); + blinkers.push_back (Output); + } + } +} + +void +CC121::ardour_pan_azimuth (float delta) +{ + if (!_current_stripable) { + return; + } + + boost::shared_ptr r = boost::dynamic_pointer_cast (_current_stripable); + + if (!r) { + return; + } + + boost::shared_ptr pannable = r->pannable (); + + if (!pannable) { + return; + } + + boost::shared_ptr azimuth = pannable->pan_azimuth_control; + + if (!azimuth) { + return; + } + + azimuth->set_value (azimuth->interface_to_internal (azimuth->internal_to_interface (azimuth->get_value()) + (delta)), Controllable::NoGroup); +} + + +void +CC121::ardour_pan_width(float delta) +{ + if (!_current_stripable) { + return; + } + + boost::shared_ptr r = boost::dynamic_pointer_cast (_current_stripable); + + if (!r) { + return; + } + + boost::shared_ptr pannable = r->pannable (); + + if (!pannable) { + return; + } + + boost::shared_ptr width = pannable->pan_width_control; + + if (!width) { + return; + } + + width->set_value (width->interface_to_internal (width->internal_to_interface (width->get_value()) + (delta)), Controllable::NoGroup); +} + +void +CC121::mixbus_pan (float delta) +{ +#ifdef MIXBUS + if (!_current_stripable) { + return; + } + boost::shared_ptr r = boost::dynamic_pointer_cast (_current_stripable); + + if (!r) { + return; + } + + + const uint32_t port_channel_post_pan = 2; // gtk2_ardour/mixbus_ports.h + boost::shared_ptr plug = r->ch_post(); + + if (!plug) { + return; + } + + boost::shared_ptr azimuth = boost::dynamic_pointer_cast (plug->control (Evoral::Parameter (ARDOUR::PluginAutomation, 0, port_channel_post_pan))); + + if (!azimuth) { + return; + } + + azimuth->set_value (azimuth->interface_to_internal (azimuth->internal_to_interface (azimuth->get_value()) + (delta)), Controllable::NoGroup); +#endif +} + +void +CC121::punch () +{ + access_action ("Transport/TogglePunch"); +} diff --git a/libs/surfaces/cc121/wscript b/libs/surfaces/cc121/wscript new file mode 100644 index 0000000000..478834aa3a --- /dev/null +++ b/libs/surfaces/cc121/wscript @@ -0,0 +1,34 @@ +#!/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 = ''' + cc121.cc + gui.cc + cc121_interface.cc + operations.cc + ''' + obj.export_includes = ['.'] + obj.defines = [ 'PACKAGE="ardour_cc121"' ] + obj.defines += [ 'ARDOURSURFACE_DLL_EXPORTS' ] + obj.includes = [ '.', './cc121'] + obj.name = 'libardour_cc121' + obj.target = 'ardour_cc121' + 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 0c736733a1..7ab04ef9f8 100644 --- a/libs/surfaces/wscript +++ b/libs/surfaces/wscript @@ -22,6 +22,7 @@ out = 'build' children = [ 'control_protocol', 'faderport', + 'cc121', 'generic_midi', 'mackie', ] @@ -74,6 +75,7 @@ def build(bld): bld.recurse('control_protocol') bld.recurse('generic_midi') bld.recurse('faderport') + bld.recurse('cc121') bld.recurse('mackie') if bld.is_defined ('HAVE_LO'):