/* * Copyright (C) 2018-2019 Jan Lentfer * Copyright (C) 2018 Paul Davis * Copyright (C) 2018 Robin Gareus * Copyright (C) 2018 Térence Clastres * * 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 #include #include "pbd/compose.h" #include "pbd/convert.h" #include "pbd/debug.h" #include "pbd/failed_constructor.h" #include "pbd/file_utils.h" #include "pbd/search_path.h" #include "pbd/enumwriter.h" #include "midi++/parser.h" #include "ardour/amp.h" #include "ardour/async_midi_port.h" #include "ardour/audioengine.h" #include "ardour/audio_track.h" #include "ardour/debug.h" #include "ardour/midiport_manager.h" #include "ardour/midi_track.h" #include "ardour/midi_port.h" #include "ardour/route.h" #include "ardour/session.h" #include "ardour/solo_isolate_control.h" #include "ardour/tempo.h" #include "ardour/types_convert.h" #include "ardour/vca.h" #include "ardour/vca_manager.h" #include "ardour/well_known_enum.h" #include "gtkmm2ext/gui_thread.h" #include "gui.h" #include "launch_control_xl.h" #include "pbd/i18n.h" #ifdef PLATFORM_WINDOWS #define random() rand() #endif using namespace ARDOUR; using namespace std; using namespace PBD; using namespace Glib; using namespace ArdourSurface; #include "pbd/abstract_ui.cc" // instantiate template /* init global object */ LaunchControlXL* lcxl = 0; LaunchControlXL::LaunchControlXL (ARDOUR::Session& s) : ControlProtocol (s, string (X_("Novation Launch Control XL"))) , AbstractUI (name()) , in_use (false) , _track_mode(TrackMute) , _template_number(8) // default template (factory 1) , _fader8master (false) , _device_mode (false) #ifdef MIXBUS , _ctrllowersends (false) , _fss_is_mixbus (false) #endif , _refresh_leds_flag (false) , _send_bank_base (0) , bank_start (0) , connection_state (ConnectionState (0)) , gui (0) , in_range_select (false) { lcxl = this; /* we're going to need this */ /* master cannot be removed, so no need to connect to going-away signal */ master = session->master_out (); run_event_loop (); /* Ports exist for the life of this instance */ ports_acquire (); /* Catch port connections and disconnections */ ARDOUR::AudioEngine::instance()->PortConnectedOrDisconnected.connect (port_connection, MISSING_INVALIDATOR, std::bind (&LaunchControlXL::connection_handler, this, _1, _2, _3, _4, _5), this); session->RouteAdded.connect (session_connections, MISSING_INVALIDATOR, std::bind (&LaunchControlXL::stripables_added, this), lcxl); session->vca_manager().VCAAdded.connect (session_connections, MISSING_INVALIDATOR, std::bind (&LaunchControlXL::stripables_added, this), lcxl); } LaunchControlXL::~LaunchControlXL () { DEBUG_TRACE (DEBUG::LaunchControlXL, "Launch Control XL control surface object being destroyed\n"); /* do this before stopping the event loop, so that we don't get any notifications */ port_connection.disconnect (); session_connections.drop_connections (); stripable_connections.drop_connections (); stop_using_device (); ports_release (); stop_event_loop (); tear_down_gui (); } void LaunchControlXL::run_event_loop () { DEBUG_TRACE (DEBUG::LaunchControlXL, "start event loop\n"); BaseUI::run (); } void LaunchControlXL::stop_event_loop () { DEBUG_TRACE (DEBUG::LaunchControlXL, "stop event loop\n"); BaseUI::quit (); } int LaunchControlXL::begin_using_device () { DEBUG_TRACE (DEBUG::LaunchControlXL, "begin using device\n"); switch_template(template_number()); // first factory template connect_session_signals (); build_maps(); reset(template_number()); init_buttons (true); init_knobs (); button_track_mode(track_mode()); set_send_bank(0); in_use = true; DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose("fader8master inital value '%1'\n", fader8master())); if (fader8master()) { set_fader8master (fader8master()); } #ifdef MIXBUS if (ctrllowersends()) { set_ctrllowersends (ctrllowersends()); } #endif return 0; } int LaunchControlXL::stop_using_device () { DEBUG_TRACE (DEBUG::LaunchControlXL, "stop using device\n"); if (!in_use) { DEBUG_TRACE (DEBUG::LaunchControlXL, "nothing to do, device not in use\n"); return 0; } init_buttons (false); session_connections.drop_connections (); in_use = false; return 0; } int LaunchControlXL::ports_acquire () { DEBUG_TRACE (DEBUG::LaunchControlXL, "acquiring ports\n"); /* setup ports */ _async_in = AudioEngine::instance()->register_input_port (DataType::MIDI, X_("Launch Control XL in"), true); _async_out = AudioEngine::instance()->register_output_port (DataType::MIDI, X_("Launch Control XL out"), true); if (_async_in == 0 || _async_out == 0) { DEBUG_TRACE (DEBUG::LaunchControlXL, "cannot register ports\n"); return -1; } /* We do not add our ports to the input/output bundles because we don't * want users wiring them by hand. They could use JACK tools if they * really insist on that (and use JACK) */ _input_port = std::dynamic_pointer_cast(_async_in).get(); _output_port = std::dynamic_pointer_cast(_async_out).get(); session->BundleAddedOrRemoved (); connect_to_parser (); /* Connect input port to event loop */ AsyncMIDIPort* asp; asp = static_cast (_input_port); asp->xthread().set_receive_handler (sigc::bind (sigc::mem_fun (this, &LaunchControlXL::midi_input_handler), _input_port)); asp->xthread().attach (main_loop()->get_context()); return 0; } void LaunchControlXL::ports_release () { DEBUG_TRACE (DEBUG::LaunchControlXL, "releasing ports\n"); /* wait for button data to be flushed */ AsyncMIDIPort* asp; asp = static_cast (_output_port); asp->drain (10000, 500000); { Glib::Threads::Mutex::Lock em (AudioEngine::instance()->process_lock()); AudioEngine::instance()->unregister_port (_async_in); AudioEngine::instance()->unregister_port (_async_out); } _async_in.reset ((ARDOUR::Port*) 0); _async_out.reset ((ARDOUR::Port*) 0); _input_port = 0; _output_port = 0; } list > LaunchControlXL::bundles () { list > b; if (_output_bundle) { b.push_back (_output_bundle); } return b; } void LaunchControlXL::init_knobs_and_buttons() { init_knobs(); init_buttons(); } void LaunchControlXL::init_buttons() { init_buttons(false); } void LaunchControlXL::init_buttons (ButtonID buttons[], uint8_t i) { DEBUG_TRACE (DEBUG::LaunchControlXL, "init_buttons buttons[]\n"); for (uint8_t n = 0; n < i; ++n) { std::shared_ptr button = std::dynamic_pointer_cast (id_note_button_map[buttons[n]]); if (button) { switch ((button->check_method)()) { case (dev_nonexistant): button->set_color(Off); break ; case (dev_inactive): button->set_color(button->color_disabled()); break; case (dev_active): button->set_color(button->color_enabled()); break; } DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("Button %1 check_method returned: %2\n", n, (int)button->check_method())); DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("Write state_msg for Button:%1\n", n)); write (button->state_msg()); } } /* set "Track Select" LEDs always on - we cycle through stripables */ std::shared_ptr sl = std::dynamic_pointer_cast(id_controller_button_map[SelectLeft]); std::shared_ptr sr = std::dynamic_pointer_cast(id_controller_button_map[SelectRight]); if (sl && sr) { write(sl->state_msg(true)); write(sr->state_msg(true)); } std::shared_ptr db = std::dynamic_pointer_cast(id_note_button_map[Device]); if (db) { write(db->state_msg(device_mode())); } } void LaunchControlXL::init_buttons (bool startup) { DEBUG_TRACE (DEBUG::LaunchControlXL, "init_buttons (bool startup)\n"); if (startup && !device_mode()) { switch_bank(bank_start); return; } if (device_mode()) { ButtonID buttons[] = { Focus1, Focus2, Focus3, Focus4, Focus5, Focus6, Focus7, Focus8, Control1, Control2, Control3, Control4, Control5, Control6, Control7, Control8 }; for (size_t n = 0; n < sizeof (buttons) / sizeof (buttons[0]); ++n) { std::shared_ptr button = std::dynamic_pointer_cast (id_note_button_map[buttons[n]]); if (button) { switch ((button->check_method)()) { case (dev_nonexistant): button->set_color(Off); break; case (dev_inactive): button->set_color(button->color_disabled()); break; case (dev_active): button->set_color(button->color_enabled()); break; } DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("Button %1 check_method returned: %2\n", n, (int)button->check_method())); DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("Write state_msg for Button:%1\n", n)); write (button->state_msg()); } } } /* set "Track Select" LEDs always on - we cycle through stripables */ std::shared_ptr sl = std::dynamic_pointer_cast(id_controller_button_map[SelectLeft]); std::shared_ptr sr = std::dynamic_pointer_cast(id_controller_button_map[SelectRight]); if (sl && sr) { write(sl->state_msg(true)); write(sr->state_msg(true)); } #ifdef MIXBUS // for now we only offer a device mode for Mixbus std::shared_ptr db = std::dynamic_pointer_cast(id_note_button_map[Device]); if (db) { write(db->state_msg(device_mode())); } #endif } void LaunchControlXL::init_knobs (KnobID knobs[], uint8_t i) { for (uint8_t n = 0; n < i ; ++n) { DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("init_knobs from array - n:%1\n", n)); std::shared_ptr knob = id_knob_map[knobs[n]]; if (knob) { switch ((knob->check_method)()) { case (dev_nonexistant): knob->set_color(Off); break; case (dev_inactive): knob->set_color(knob->color_disabled()); break; case (dev_active): knob->set_color(knob->color_enabled()); break; } DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("Write state_msg for Knob:%1\n", n)); write (knob->state_msg()); } } } void LaunchControlXL::init_knobs () { if (!device_mode()) { for (int n = 0; n < 8; ++n) { update_knob_led_by_strip(n); } } else { KnobID knobs[] = { SendA1, SendA2, SendA3, SendA4, SendA5, SendA6, SendA7, SendA8, SendB1, SendB2, SendB3, SendB4, SendB5, SendB6, SendB7, SendB8, Pan1, Pan2, Pan3, Pan4, Pan5, Pan6, Pan7, Pan8 }; for (size_t n = 0; n < sizeof (knobs) / sizeof (knobs[0]); ++n) { std::shared_ptr knob = id_knob_map[knobs[n]]; if (knob) { switch ((knob->check_method)()) { case (dev_nonexistant): knob->set_color(Off); break; case (dev_inactive): knob->set_color(knob->color_disabled()); break; case (dev_active): knob->set_color(knob->color_enabled()); break; } DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("Write state_msg for Knob:%1\n", n)); write (knob->state_msg()); } } } } void LaunchControlXL::do_request (LaunchControlRequest * req) { if (req->type == CallSlot) { call_slot (MISSING_INVALIDATOR, req->the_slot); } else if (req->type == Quit) { stop_using_device (); } } void LaunchControlXL::reset(uint8_t chan) { MidiByteArray msg (3, 176 + chan, 0, 0); // turn off all leds, reset buffer settings and duty cycle write(msg); } int LaunchControlXL::set_active (bool yn) { DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose("LaunchControlProtocol::set_active init with yn: '%1'\n", yn)); if (yn == active()) { return 0; } if (yn) { if ((connection_state & (InputConnected|OutputConnected)) == (InputConnected|OutputConnected)) { begin_using_device (); } else { /* begin_using_device () will get called once we're connected */ } } else { /* Control Protocol Manager never calls us with false, but * insteads destroys us. */ } ControlProtocol::set_active (yn); DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose("LaunchControlProtocol::set_active done with yn: '%1'\n", yn)); return 0; } void LaunchControlXL::write (const MidiByteArray& data) { /* immediate delivery */ _output_port->write (&data[0], data.size(), 0); } /* Device to Ardour message handling */ bool LaunchControlXL::midi_input_handler (IOCondition ioc, MIDI::Port* port) { if (ioc & ~IO_IN) { DEBUG_TRACE (DEBUG::LaunchControlXL, "MIDI port closed\n"); return false; } if (ioc & IO_IN) { DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("something happened on %1\n", port->name())); AsyncMIDIPort* asp = static_cast(port); if (asp) { asp->clear (); } DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("data available on %1\n", port->name())); if (in_use) { samplepos_t now = AudioEngine::instance()->sample_time(); port->parse (now); } } return true; } void LaunchControlXL::connect_to_parser () { DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("Connecting to signals on port %1\n", _input_port->name())); MIDI::Parser* p = _input_port->parser(); /* Incoming sysex */ p->sysex.connect_same_thread (*this, std::bind (&LaunchControlXL::handle_midi_sysex, this, _1, _2, _3)); for (MIDI::channel_t n = 0; n < 16; ++n) { /* Controller */ p->channel_controller[(int)n].connect_same_thread (*this, std::bind (&LaunchControlXL::handle_midi_controller_message, this, _1, _2, n)); /* Button messages are NoteOn */ p->channel_note_on[(int)n].connect_same_thread (*this, std::bind (&LaunchControlXL::handle_midi_note_on_message, this, _1, _2, n)); /* Button messages are NoteOn but libmidi++ sends note-on w/velocity = 0 as note-off so catch them too */ p->channel_note_off[(int)n].connect_same_thread (*this, std::bind (&LaunchControlXL::handle_midi_note_off_message, this, _1, _2, n)); } } void LaunchControlXL::handle_midi_sysex (MIDI::Parser&, MIDI::byte* raw_bytes, size_t sz) { DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("Sysex, %1 bytes\n", sz)); if (sz < 8) { return; } MidiByteArray msg (sz, raw_bytes); MidiByteArray lcxl_sysex_header (6, 0xF0, 0x00, 0x20, 0x29, 0x02, 0x11); if (!lcxl_sysex_header.compare_n (msg, 6)) { return; } switch (msg[6]) { case 0x77: /* template change */ DEBUG_TRACE (DEBUG::LaunchControlXL, string_compose ("Template change: %1\n", (int)msg[7])); _template_number = msg[7]; bank_start = 0; if (!device_mode ()) { switch_bank(bank_start); } else { init_device_mode(); } break; } } void LaunchControlXL::handle_button_message(std::shared_ptr