From 3e34229b9f0d7b9adaef5cb0e95dce1d8a97eaa7 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Thu, 19 Oct 2023 22:18:34 -0600 Subject: [PATCH] initial launchpad X exploration, using copy-n-edit of launchpad pro code --- libs/surfaces/launchpad_x/gui.cc | 272 +++ libs/surfaces/launchpad_x/gui.h | 101 ++ libs/surfaces/launchpad_x/interface.cc | 82 + libs/surfaces/launchpad_x/lpx.cc | 2089 ++++++++++++++++++++++++ libs/surfaces/launchpad_x/lpx.h | 482 ++++++ libs/surfaces/launchpad_x/wscript | 30 + 6 files changed, 3056 insertions(+) create mode 100644 libs/surfaces/launchpad_x/gui.cc create mode 100644 libs/surfaces/launchpad_x/gui.h create mode 100644 libs/surfaces/launchpad_x/interface.cc create mode 100644 libs/surfaces/launchpad_x/lpx.cc create mode 100644 libs/surfaces/launchpad_x/lpx.h create mode 100644 libs/surfaces/launchpad_x/wscript diff --git a/libs/surfaces/launchpad_x/gui.cc b/libs/surfaces/launchpad_x/gui.cc new file mode 100644 index 0000000000..303ca0c5c7 --- /dev/null +++ b/libs/surfaces/launchpad_x/gui.cc @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2016 Paul Davis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include + +#include "pbd/unwind.h" +#include "pbd/strsplit.h" +#include "pbd/file_utils.h" + +#include "gtkmm2ext/bindings.h" +#include "gtkmm2ext/gui_thread.h" +#include "gtkmm2ext/utils.h" + +#include "ardour/audioengine.h" +#include "ardour/filesystem_paths.h" +#include "ardour/parameter_descriptor.h" + +#include "lpx.h" +#include "gui.h" + +#include "pbd/i18n.h" + +using namespace PBD; +using namespace ARDOUR; +using namespace ArdourSurface; +using namespace Gtk; +using namespace Gtkmm2ext; + +void* +LaunchPadX::get_gui () const +{ + if (!_gui) { + const_cast(this)->build_gui (); + } + + static_cast(_gui)->show_all(); + return _gui; +} + +void +LaunchPadX::tear_down_gui () +{ + if (_gui) { + Gtk::Widget *w = static_cast(_gui)->get_parent(); + if (w) { + w->hide(); + delete w; + } + } + delete _gui; + _gui = 0; +} + +void +LaunchPadX::build_gui () +{ + _gui = new LPX_GUI (*this); +} + +/*--------------------*/ + +LPX_GUI::LPX_GUI (LaunchPadX& p) + : _lp (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; + std::string name = "launchpad-pro.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; + int row = 0; + + _input_combo.pack_start (_midi_port_columns.short_name); + _output_combo.pack_start (_midi_port_columns.short_name); + + _input_combo.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &LPX_GUI::active_port_changed), &_input_combo, true)); + _output_combo.signal_changed().connect (sigc::bind (sigc::mem_fun (*this, &LPX_GUI::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++; + + _hpacker.pack_start (_table, true, true); + + set_spacing (12); + + pack_start (_hpacker, false, false); + + /* update the port connection combos */ + + update_port_combos (); + + /* catch future changes to connection state */ + + ARDOUR::AudioEngine::instance()->PortRegisteredOrUnregistered.connect (_port_connections, invalidator (*this), boost::bind (&LPX_GUI::connection_handler, this), gui_context()); + ARDOUR::AudioEngine::instance()->PortPrettyNameChanged.connect (_port_connections, invalidator (*this), boost::bind (&LPX_GUI::connection_handler, this), gui_context()); + _lp.ConnectionChange.connect (_port_connections, invalidator (*this), boost::bind (&LPX_GUI::connection_handler, this), gui_context()); +} + +LPX_GUI::~LPX_GUI () +{ +} + +void +LPX_GUI::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 +LPX_GUI::update_port_combos () +{ + std::vector midi_inputs; + std::vector midi_outputs; + + if (!_lp.input_port() || !_lp.output_port()) { + return; + } + + 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) { + std::string port_name = (*i)[_midi_port_columns.full_name]; + if (_lp.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) { + std::string port_name = (*i)[_midi_port_columns.full_name]; + if (_lp.output_port()->connected_to (port_name)) { + _output_combo.set_active (n); + output_found = true; + break; + } + } + + if (!output_found) { + _output_combo.set_active (0); /* disconnected */ + } +} + +Glib::RefPtr +LPX_GUI::build_midi_port_list (std::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] = std::string(); + row[_midi_port_columns.short_name] = _("Disconnected"); + + for (std::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 +LPX_GUI::active_port_changed (Gtk::ComboBox* combo, bool for_input) +{ + if (_ignore_active_change) { + return; + } + + TreeModel::iterator active = combo->get_active (); + std::string new_port = (*active)[_midi_port_columns.full_name]; + + if (new_port.empty()) { + if (for_input) { + _lp.input_port()->disconnect_all (); + } else { + _lp.output_port()->disconnect_all (); + } + + return; + } + + if (for_input) { + if (!_lp.input_port()->connected_to (new_port)) { + _lp.input_port()->disconnect_all (); + _lp.input_port()->connect (new_port); + } + } else { + if (!_lp.output_port()->connected_to (new_port)) { + _lp.output_port()->disconnect_all (); + _lp.output_port()->connect (new_port); + } + } +} diff --git a/libs/surfaces/launchpad_x/gui.h b/libs/surfaces/launchpad_x/gui.h new file mode 100644 index 0000000000..ffd69a4a99 --- /dev/null +++ b/libs/surfaces/launchpad_x/gui.h @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2016 Paul Davis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __ardour_lpx_gui_h__ +#define __ardour_lpx_gui_h__ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gtk { + class ListStore; +} + +#include "ardour/mode.h" + +#include "lpx.h" + +namespace ArdourSurface { + +class LPX_GUI : public Gtk::VBox +{ +public: + LPX_GUI (LaunchPadX&); + ~LPX_GUI (); + +private: + LaunchPadX& _lp; + Gtk::HBox _hpacker; + Gtk::Table _table; + Gtk::Table _action_table; + Gtk::ComboBox _input_combo; + Gtk::ComboBox _output_combo; + Gtk::Image _image; + + void update_port_combos (); + void connection_handler (); + + PBD::ScopedConnectionList _port_connections; + + 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); + +#if 0 + struct PressureModeColumns : public Gtk::TreeModel::ColumnRecord { + PressureModeColumns() { + add (mode); + add (name); + } + Gtk::TreeModelColumn mode; + Gtk::TreeModelColumn name; + }; + + PressureModeColumns _pressure_mode_columns; + Glib::RefPtr build_pressure_mode_columns (); + Gtk::ComboBox _pressure_mode_selector; + Gtk::Label _pressure_mode_label; + + void reprogram_pressure_mode (); +#endif +}; + +} + +#endif /* __ardour_lpx_gui_h__ */ diff --git a/libs/surfaces/launchpad_x/interface.cc b/libs/surfaces/launchpad_x/interface.cc new file mode 100644 index 0000000000..e773bcd417 --- /dev/null +++ b/libs/surfaces/launchpad_x/interface.cc @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2016 Paul Davis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include + +#include "pbd/error.h" + +#include "ardour/rc_configuration.h" + +#include "control_protocol/control_protocol.h" +#include "lpx.h" + +using namespace ARDOUR; +using namespace PBD; +using namespace ArdourSurface; + +static ControlProtocol* +new_lpx (Session* s) +{ + LaunchPadX * p2 = 0; + + try { + p2 = new LaunchPadX (*s); + /* do not set active here - wait for set_state() */ + } + catch (std::exception & e) { + error << "Error instantiating LaunchPad X support: " << e.what() << endmsg; + delete p2; + p2 = 0; + } + + return p2; +} + +static void +delete_lpx (ControlProtocol* cp) +{ + try + { + delete cp; + } + catch ( std::exception & e ) + { + std::cout << "Exception caught trying to finalize LaunchPad X support: " << e.what() << std::endl; + } +} + +static bool +probe_lpx_midi_protocol () +{ + std::string i, o; + return LaunchPadX::probe (i, o); +} + + +static ControlProtocolDescriptor lpx_descriptor = { + /* name */ "Novation LaunchPad X", + /* id */ "uri://ardour.org/surfaces/lpx:0", + /* module */ 0, + /* available */ LaunchPadX::available, + /* probe_port */ probe_lpx_midi_protocol, + /* match usb */ 0, // LaunchPadX::match_usb, + /* initialize */ new_lpx, + /* destroy */ delete_lpx, +}; + +extern "C" ARDOURSURFACE_API ControlProtocolDescriptor* protocol_descriptor () { return &lpx_descriptor; } diff --git a/libs/surfaces/launchpad_x/lpx.cc b/libs/surfaces/launchpad_x/lpx.cc new file mode 100644 index 0000000000..bd303cb5dd --- /dev/null +++ b/libs/surfaces/launchpad_x/lpx.cc @@ -0,0 +1,2089 @@ +/* + * Copyright (C) 2016-2018 Paul Davis + * Copyright (C) 2017-2018 Robin Gareus + * + * 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 +#include +#include + +#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 "temporal/time.h" +#include "temporal/bbt_time.h" + +#include "ardour/amp.h" +#include "ardour/async_midi_port.h" +#include "ardour/audioengine.h" +#include "ardour/debug.h" +#include "ardour/midiport_manager.h" +#include "ardour/midi_track.h" +#include "ardour/midi_port.h" +#include "ardour/selection.h" +#include "ardour/session.h" +#include "ardour/tempo.h" +#include "ardour/triggerbox.h" +#include "ardour/types_convert.h" +#include "ardour/utils.h" + +#include "gtkmm2ext/gui_thread.h" +#include "gtkmm2ext/rgb_macros.h" + +#include "gtkmm2ext/colors.h" + +#include "gui.h" +#include "lpx.h" + +#include "pbd/i18n.h" + +#ifdef PLATFORM_WINDOWS +#define random() rand() +#endif + +using namespace ARDOUR; +using namespace PBD; +using namespace Glib; +using namespace ArdourSurface; +using namespace Gtkmm2ext; + +#include "pbd/abstract_ui.cc" // instantiate template + +#define NOVATION 0x1235 +#define LAUNCHPADX 0x0103 +static const std::vector sysex_header ({ 0xf0, 0x00, 0x20, 0x29, 0x2, 0xc }); + +const LaunchPadX::PadID LaunchPadX::all_pad_ids[] = { + Shift, Left, Right, Session, Note, Chord, Custom, Sequencer, Projects, + Patterns, Steps, PatternSettings, Velocity, Probability, Mutation, MicroStep, PrintToClip, + StopClip, Device, Sends, Pan, Volume, Solo, Mute, RecordArm, + CaptureMIDI, Play, FixedLength, Quantize, Duplicate, Clear, Down, Up, + Lower1, Lower2, Lower3, Lower4, Lower5, Lower6, Lower7, Lower8, +}; + +const LaunchPadX::Layout LaunchPadX::AllLayouts[] = { + SessionLayout, Fader, ChordLayout, CustomLayout, NoteLayout, Scale, SequencerSettings, + SequencerSteps, SequencerVelocity, SequencerPatternSettings, SequencerProbability, SequencerMutation, + SequencerMicroStep, SequencerProjects, SequencerPatterns, SequencerTempo, SequencerSwing, ProgrammerLayout, Settings, CustomSettings +}; + +bool +LaunchPadX::available () +{ + /* no preconditions other than the device being present */ + return true; +} + +bool +LaunchPadX::match_usb (uint16_t vendor, uint16_t device) +{ + return vendor == NOVATION && device == LAUNCHPADX; +} + +bool +LaunchPadX::probe (std::string& i, std::string& o) +{ + vector midi_inputs; + vector midi_outputs; + + AudioEngine::instance()->get_ports ("", DataType::MIDI, PortFlags (IsOutput|IsTerminal), midi_inputs); + AudioEngine::instance()->get_ports("", DataType::MIDI, PortFlags(IsInput | IsTerminal), midi_outputs); + + if (midi_inputs.empty() || midi_outputs.empty()) { + return false; + } + + std::regex rx (X_("Launchpad X.*MIDI")); + + auto has_lppro = [&rx](string const &s) { + std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s); + return std::regex_search (pn, rx); + }; + + auto pi = std::find_if (midi_inputs.begin(), midi_inputs.end(), has_lppro); + auto po = std::find_if (midi_outputs.begin (), midi_outputs.end (), has_lppro); + + if (pi == midi_inputs.end () || po == midi_outputs.end ()) { + return false; + } + + i = *pi; + o = *po; + return true; +} + +LaunchPadX::LaunchPadX (ARDOUR::Session& s) + : MIDISurface (s, X_("Novation LaunchPad X"), X_("LaunchPad X"), true) + , logo_color (4) + , scroll_x_offset (0) + , scroll_y_offset (0) + , _daw_out_port (nullptr) + , _gui (nullptr) + , _current_layout (SessionLayout) + , _shift_pressed (false) + , _clear_pressed (false) + , _duplicate_pressed (false) + , _session_pressed (false) + , did_session_display (false) + , current_fader_bank (VolumeFaders) + , revert_layout_on_fader_release (false) + , pre_fader_layout (SessionLayout) +{ + run_event_loop (); + port_setup (); + + std::string pn_in, pn_out; + if (probe (pn_in, pn_out)) { + _async_in->connect (pn_in); + _async_out->connect (pn_out); + } + + connect_daw_ports (); + + build_color_map (); + build_pad_map (); + + Trigger::TriggerPropertyChange.connect (trigger_connections, invalidator (*this), boost::bind (&LaunchPadX::trigger_property_change, this, _1, _2), this); + + session->RecordStateChanged.connect (session_connections, invalidator(*this), boost::bind (&LaunchPadX::record_state_changed, this), this); + session->TransportStateChange.connect (session_connections, invalidator(*this), boost::bind (&LaunchPadX::transport_state_changed, this), this); + session->RouteAdded.connect (session_connections, invalidator(*this), boost::bind (&LaunchPadX::viewport_changed, this), this); +} + +LaunchPadX::~LaunchPadX () +{ + DEBUG_TRACE (DEBUG::Launchpad, "push2 control surface object being destroyed\n"); + + trigger_connections.drop_connections (); + route_connections.drop_connections (); + session_connections.drop_connections (); + + for (auto & p : pad_map) { + p.second.timeout_connection.disconnect (); + } + + stop_event_loop (); + tear_down_gui (); + + MIDISurface::drop (); + +} + +void +LaunchPadX::transport_state_changed () +{ + MIDI::byte msg[3]; + msg[0] = 0x90; + + if (session->transport_rolling()) { + msg[1] = Play; + msg[2] = 21; + daw_write (msg, 3); + } else { + msg[1] = Play; + msg[2] = 17; + daw_write (msg, 3); + } +} + +void +LaunchPadX::record_state_changed () +{ +} + +int +LaunchPadX::set_active (bool yn) +{ + DEBUG_TRACE (DEBUG::Launchpad, string_compose("Launchpad X::set_active init with yn: %1\n", yn)); + + if (yn == active()) { + return 0; + } + + if (yn) { + + if (device_acquire ()) { + return -1; + } + + } else { + /* Control Protocol Manager never calls us with false, but + * insteads destroys us. + */ + } + + ControlProtocol::set_active (yn); + + DEBUG_TRACE (DEBUG::Launchpad, string_compose("Launchpad X::set_active done with yn: '%1'\n", yn)); + + return 0; +} + +void +LaunchPadX::run_event_loop () +{ + DEBUG_TRACE (DEBUG::Launchpad, "start event loop\n"); + BaseUI::run (); +} + +void +LaunchPadX::stop_event_loop () +{ + DEBUG_TRACE (DEBUG::Launchpad, "stop event loop\n"); + BaseUI::quit (); +} + +int +LaunchPadX::begin_using_device () +{ + DEBUG_TRACE (DEBUG::Launchpad, "begin using device\n"); + + connect_to_port_parser (*_daw_in_port); + + /* Connect DAW input port to event loop */ + + AsyncMIDIPort* asp; + + asp = dynamic_cast (_daw_in_port); + asp->xthread().set_receive_handler (sigc::bind (sigc::mem_fun (this, &MIDISurface::midi_input_handler), _daw_in_port)); + asp->xthread().attach (main_loop()->get_context()); + + light_logo (); + + set_device_mode (DAW); + setup_faders (VolumeFaders); + setup_faders (PanFaders); + setup_faders (SendFaders); + setup_faders (DeviceFaders); + set_layout (SessionLayout); + + /* catch current selection, if any so that we can wire up the pads if appropriate */ + stripable_selection_changed (); + viewport_changed (); + + return MIDISurface::begin_using_device (); +} + +int +LaunchPadX::stop_using_device () +{ + DEBUG_TRACE (DEBUG::Launchpad, "stop using device\n"); + + if (!_in_use) { + DEBUG_TRACE (DEBUG::Launchpad, "nothing to do, device not in use\n"); + return 0; + } + + all_pads_out (); + set_device_mode (Standalone); + + return MIDISurface::stop_using_device (); +} + +XMLNode& +LaunchPadX::get_state() const +{ + XMLNode& node (MIDISurface::get_state()); + + XMLNode* child = new XMLNode (X_("DAWInput")); + child->add_child_nocopy (_daw_in->get_state()); + node.add_child_nocopy (*child); + child = new XMLNode (X_("DAWOutput")); + child->add_child_nocopy (_daw_out->get_state()); + node.add_child_nocopy (*child); + + return node; +} + +int +LaunchPadX::set_state (const XMLNode & node, int version) +{ + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("LaunchPadX::set_state: active %1\n", active())); + + int retval = 0; + + if (MIDISurface::set_state (node, version)) { + return -1; + } + + return retval; +} + +std::string +LaunchPadX::input_port_name () const +{ + return X_(":Launchpad X MK3.*MIDI (In|1)"); +} + +std::string +LaunchPadX::output_port_name () const +{ + return X_(":Launchpad X MK3.*MIDI (Out|1)"); +} + +void +LaunchPadX::relax (Pad & pad) +{ +} + +void +LaunchPadX::build_pad_map () +{ + +#define BUTTON0(id) pad_map.insert (make_pair ((id), Pad ((id), &LaunchPadX::relax))) +#define BUTTON(id, press) pad_map.insert (make_pair ((id), Pad ((id), (press)))) +#define BUTTON2(id, press, long_press) pad_map.insert (make_pair ((id), Pad ((id), (press), (long_press)))) +#define BUTTON3(id, press, long_press, release) pad_map.insert (make_pair ((id), Pad ((id), (press), (long_press), (release)))) + + BUTTON3 (Shift, &LaunchPadX::shift_press, &LaunchPadX::relax, &LaunchPadX::shift_release); + + BUTTON (Left, &LaunchPadX::left_press); + BUTTON (Right, &LaunchPadX::right_press); + BUTTON3 (Session, &LaunchPadX::session_press, &LaunchPadX::session_long_press, &LaunchPadX::session_release); + BUTTON0 (Note); + BUTTON0 (Chord); + BUTTON0 (Custom); + BUTTON0 (Sequencer); + BUTTON0 (Projects); + + BUTTON (Patterns, &LaunchPadX::patterns_press); + BUTTON (Steps, &LaunchPadX::steps_press); + BUTTON (PatternSettings, &LaunchPadX::pattern_settings_press); + BUTTON (Velocity, &LaunchPadX::velocity_press); + BUTTON (Probability, &LaunchPadX::probability_press); + BUTTON (Mutation, &LaunchPadX::mutation_press); + BUTTON (MicroStep, &LaunchPadX::microstep_press); + BUTTON (PrintToClip, &LaunchPadX::print_to_clip_press); + + BUTTON (StopClip, &LaunchPadX::stop_clip_press); + BUTTON3 (Device, &LaunchPadX::device_press, &LaunchPadX::fader_long_press, &LaunchPadX::fader_release); + BUTTON3 (Sends, &LaunchPadX::sends_press, &LaunchPadX::fader_long_press, &LaunchPadX::fader_release); + BUTTON3 (Pan, &LaunchPadX::pan_press, &LaunchPadX::fader_long_press, &LaunchPadX::fader_release); + BUTTON3 (Volume, &LaunchPadX::volume_press, &LaunchPadX::fader_long_press, &LaunchPadX::fader_release); + BUTTON2 (Solo, &LaunchPadX::solo_press, &LaunchPadX::solo_long_press); + BUTTON (Mute, &LaunchPadX::mute_press); + BUTTON (RecordArm, &LaunchPadX::record_arm_press); + + BUTTON (CaptureMIDI, &LaunchPadX::capture_midi_press); + BUTTON (Play, &LaunchPadX::play_press); + BUTTON0 (FixedLength); + BUTTON0 (Quantize); + BUTTON3 (Duplicate, &LaunchPadX::duplicate_press, &LaunchPadX::duplicate_long_press, &LaunchPadX::duplicate_release); + BUTTON3 (Clear, &LaunchPadX::clear_press, &LaunchPadX::clear_long_press, &LaunchPadX::clear_release); + BUTTON (Down, &LaunchPadX::down_press); + BUTTON (Up, &LaunchPadX::up_press); + + BUTTON (Lower1, &LaunchPadX::lower1_press); + BUTTON (Lower2, &LaunchPadX::lower2_press); + BUTTON (Lower3, &LaunchPadX::lower3_press); + BUTTON (Lower4, &LaunchPadX::lower4_press); + BUTTON (Lower5, &LaunchPadX::lower5_press); + BUTTON (Lower6, &LaunchPadX::lower6_press); + BUTTON (Lower7, &LaunchPadX::lower7_press); + BUTTON (Lower8, &LaunchPadX::lower8_press); + + /* Now add the 8x8 central pad grid */ + + for (int row = 0; row < 8; ++row) { + for (int col = 0; col < 8; ++col) { + int pid = (11 + (row * 10)) + col; + std::pair p (pid, Pad (pid, col, 7 - row, &LaunchPadX::pad_press, &LaunchPadX::pad_long_press, &LaunchPadX::relax)); + if (!pad_map.insert (p).second) abort(); + } + } + + /* The +1 is for the shift pad at upper left */ + assert (pad_map.size() == (64 + (5 * 8) + 1)); +} + +void +LaunchPadX::all_pads_out () +{ + MIDI::byte msg[3]; + msg[0] = 0x90; + msg[2] = 0x0; + + for (auto const & p : pad_map) { + msg[1] = p.second.id; + daw_write (msg, 3); + } + + /* Finally, the logo */ + msg[1] = 0x63; + daw_write (msg, 3); +} + + +bool +LaunchPadX::light_logo () +{ + MIDI::byte msg[3]; + + msg[0] = 0x91; /* pulse with tempo/midi clock */ + msg[1] = 0x63; + msg[2] = 4 + (random() % 0x3c); + + daw_write (msg, 3); + + return true; +} + +LaunchPadX::Pad* +LaunchPadX::pad_by_id (int pid) +{ + PadMap::iterator p = pad_map.find (pid); + if (p == pad_map.end()) { + return nullptr; + } + return &p->second; +} + +void +LaunchPadX::light_pad (int pad_id, int color, int mode) +{ + MIDI::byte msg[3]; + msg[0] = 0x90 | mode; + msg[1] = pad_id; + msg[2] = color; + daw_write (msg, 3); +} + +void +LaunchPadX::pad_off (int pad_id) +{ + MIDI::byte msg[3]; + msg[0] = 0x90; + msg[1] = pad_id; + msg[2] = 0; + daw_write (msg, 3); +} + +void +LaunchPadX::all_pads_off () +{ + MidiByteArray msg (sysex_header); + msg.reserve (msg.size() + (106 * 3) + 3); + msg.push_back (0x3); + for (size_t n = 1; n < 32; ++n) { + msg.push_back (0x0); + msg.push_back (n); + msg.push_back (13); + } + msg.push_back (0xf7); + daw_write (msg); +} + +void +LaunchPadX::all_pads_on (int color) +{ + MidiByteArray msg (sysex_header); + msg.push_back (0xe); + msg.push_back (color & 0x7f); + msg.push_back (0xf7); + daw_write (msg); + +#if 0 + for (PadMap::iterator p = pad_map.begin(); p != pad_map.end(); ++p) { + Pad& pad (p->second); + pad.set (random() % color_map.size(), Pad::Static); + daw_write (pad.state_msg()); + } +#endif +} + +void +LaunchPadX::set_layout (Layout l, int page) +{ + MidiByteArray msg (sysex_header); + msg.push_back (0x0); + msg.push_back (l); + msg.push_back (page); + msg.push_back (0x0); + msg.push_back (0xf7); + daw_write (msg); + + if (l == Fader) { + pre_fader_layout = _current_layout; + current_fader_bank = (FaderBank) page; + } +} + +void +LaunchPadX::set_device_mode (DeviceMode m) +{ + /* programming manual, pages 14 and 18 */ + MidiByteArray standalone_or_daw (sysex_header); + MidiByteArray live_or_programmer (sysex_header); + + switch (m) { + case Standalone: + live_or_programmer.push_back (0xe); + live_or_programmer.push_back (0x0); + live_or_programmer.push_back (0xf7); + /* Back to "live" state */ + write (live_or_programmer); + g_usleep (100000); + /* disable "daw" mode */ + standalone_or_daw.push_back (0x10); + standalone_or_daw.push_back (0x0); + standalone_or_daw.push_back (0xf7); + daw_write (standalone_or_daw); + break; + + case DAW: + // live_or_programmer.push_back (0xe); + // live_or_programmer.push_back (0x0); + // live_or_programmer.push_back (0xf7); + /* Back to "live" state */ + // daw_write (live_or_programmer); + // g_usleep (100000); + /* Enable DAW mode */ + standalone_or_daw.push_back (0x10); + standalone_or_daw.push_back (0x1); + standalone_or_daw.push_back (0xf7); + daw_write (standalone_or_daw); + break; + + case Programmer: + live_or_programmer.push_back (0xe); + live_or_programmer.push_back (0x1); + live_or_programmer.push_back (0xf7); + /* enter "programmer" state */ + daw_write (live_or_programmer); + break; + } +} + +void +LaunchPadX::handle_midi_sysex (MIDI::Parser& parser, MIDI::byte* raw_bytes, size_t sz) +{ + MidiByteArray m (sz, raw_bytes); + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("Sysex, %1 bytes parser %2 %s\n", sz, &parser, m)); + + if (&parser != _daw_in_port->parser()) { + DEBUG_TRACE (DEBUG::Launchpad, "sysex from non-DAW port, ignored\n"); + return; + } + + if (sz < sysex_header.size() + 1) { + return; + } + + const size_t num_layouts = sizeof (AllLayouts) / sizeof (AllLayouts[0]); + + raw_bytes += sysex_header.size(); + + switch (raw_bytes[0]) { + case 0x0: /* layout info */ + if (sz < sysex_header.size() + 2) { + return; + } + + if (raw_bytes[1] < num_layouts) { + _current_layout = AllLayouts[raw_bytes[1]]; + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("new layout: %1\n", _current_layout)); + switch (_current_layout) { + case SessionLayout: + display_session_layout (); + map_triggers (); + break; + case Fader: + map_faders (); + break; + default: + break; + } + stripable_selection_changed (); + } else { + std::cerr << "ignore illegal layout index " << (int) raw_bytes[1] << std::endl; + } + break; + default: + break; + } +} + +void +LaunchPadX::display_session_layout () +{ + /* This only needs to be done once (in fact, the device even remembers + * it across power-cycling! + */ + + if (did_session_display) { + return; + } + + MIDI::byte msg[3]; + msg[0] = 0x90; + + msg[1] = Patterns; + msg[2] = 0x27; + daw_write (msg, 3); + msg[1] = Steps; + msg[2] = 0x27; + daw_write (msg, 3); + msg[1] = PatternSettings; + msg[2] = 0x27; + daw_write (msg, 3); + msg[1] = Velocity; + msg[2] = 0x27; + daw_write (msg, 3); + msg[1] = Probability; + msg[2] = 0x27; + daw_write (msg, 3); + msg[1] = Mutation; + msg[2] = 0x27; + daw_write (msg, 3); + msg[1] = MicroStep; + msg[2] = 0x27; + daw_write (msg, 3); + msg[1] = PrintToClip; + msg[2] = 0x27; + daw_write (msg, 3); + + msg[1] = Duplicate; + msg[2] = 79; + daw_write (msg, 3); + + msg[1] = Clear; + msg[2] = 3; + daw_write (msg, 3); + + msg[1] = Play; + msg[2] = 17; + daw_write (msg, 3); + + msg[1] = CaptureMIDI; + msg[2] = 5; + daw_write (msg, 3); + + msg[1] = Up; + msg[2] = 46; + daw_write (msg, 3); + msg[1] = Down; + msg[2] = 46; + daw_write (msg, 3); + msg[1] = Left; + msg[2] = 46; + daw_write (msg, 3); + msg[1] = Right; + msg[2] = 46; + daw_write (msg, 3); + + + msg[1] = StopClip; + msg[2] = 2; + daw_write (msg, 3); + msg[1] = Device; + msg[2] = 2; + daw_write (msg, 3); + msg[1] = Sends; + msg[2] = 2; + daw_write (msg, 3); + msg[1] = Pan; + msg[2] = 2; + daw_write (msg, 3); + msg[1] = Volume; + msg[2] = 2; + daw_write (msg, 3); + msg[1] = Solo; + msg[2] = 2; + daw_write (msg, 3); + msg[1] = Mute; + msg[2] = 2; + daw_write (msg, 3); + msg[1] = RecordArm; + msg[2] = 2; + daw_write (msg, 3); +} + +void +LaunchPadX::handle_midi_controller_message (MIDI::Parser&, MIDI::EventTwoBytes* ev) +{ + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("CC %1 (value %2)\n", (int) ev->controller_number, (int) ev->value)); + + if (_current_layout == Fader) { + /* Trap fader move messages and act on them */ + if (ev->controller_number >= 0x20 && ev->controller_number < 0x28) { + fader_move (ev->controller_number, ev->value); + return; + } + } + + PadMap::iterator p = pad_map.find (ev->controller_number); + if (p == pad_map.end()) { + return; + } + + Pad& pad (p->second); + + set::iterator c = consumed.find (pad.id); + + if (c == consumed.end()) { + if (ev->value) { + maybe_start_press_timeout (pad); + (this->*pad.on_press) (pad); + } else { + pad.timeout_connection.disconnect (); + (this->*pad.on_release) (pad); + } + } else { + consumed.erase (c); + } +} + +void +LaunchPadX::handle_midi_note_on_message (MIDI::Parser& parser, MIDI::EventTwoBytes* ev) +{ + if (ev->velocity == 0) { + handle_midi_note_off_message (parser, ev); + return; + } + + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("Note On %1/0x%3%4%5 (velocity %2)\n", (int) ev->note_number, (int) ev->velocity, std::hex, (int) ev->note_number, std::dec)); + + if (_current_layout != SessionLayout) { + return; + } + + PadMap::iterator p = pad_map.find (ev->note_number); + if (p == pad_map.end()) { + return; + } + + Pad& pad (p->second); + maybe_start_press_timeout (pad); + (this->*pad.on_pad_press) (pad, ev->velocity); +} + +void +LaunchPadX::handle_midi_note_off_message (MIDI::Parser&, MIDI::EventTwoBytes* ev) +{ + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("Note Off %1/0x%3%4%5 (velocity %2)\n", (int) ev->note_number, (int) ev->velocity, std::hex, (int) ev->note_number, std::dec)); + + if (_current_layout != SessionLayout) { + return; + } + + PadMap::iterator p = pad_map.find (ev->note_number); + if (p == pad_map.end()) { + return; + } + + Pad& pad (p->second); + + set::iterator c = consumed.find (pad.id); + + if (c == consumed.end()) { + pad.timeout_connection.disconnect (); + (this->*pad.on_release) (pad); + } else { + /* used for long press */ + consumed.erase (c); + } + +} + +void +LaunchPadX::port_registration_handler () +{ + MIDISurface::port_registration_handler (); + connect_daw_ports (); +} + +void +LaunchPadX::connect_daw_ports () +{ + if (!_daw_in || !_daw_out) { + /* ports not registered yet */ + std::cerr << "no daw port registered\n"; + return; + } + + if (_daw_in->connected() && _daw_out->connected()) { + /* don't waste cycles here */ + return; + } + + std::vector midi_inputs; + std::vector midi_outputs; + + /* get all MIDI Ports */ + + AudioEngine::instance()->get_ports ("", DataType::MIDI, PortFlags (IsOutput|IsTerminal), midi_inputs); + AudioEngine::instance()->get_ports("", DataType::MIDI, PortFlags(IsInput | IsTerminal), midi_outputs); + + if (midi_inputs.empty() || midi_outputs.empty()) { + return; + } + + /* Try to find the DAW port, whose pretty name varies on Linux + * depending on the version of ALSA, but is fairly consistent across + * newer ALSA and other platforms. + */ + + std::regex rx (X_("Launchpad X.*(DAW|MIDI 2)"), std::regex::extended); + + auto is_dawport = [&rx](string const &s) { + std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s); + return std::regex_search (pn, rx); + }; + + auto pi = std::find_if (midi_inputs.begin(), midi_inputs.end(), is_dawport); + auto po = std::find_if (midi_outputs.begin (), midi_outputs.end (), is_dawport); + + if (pi == midi_inputs.end() || po == midi_inputs.end()) { + return; + } + + if (!_daw_in->connected()) { + AudioEngine::instance()->connect (_daw_in->name(), *pi); + } + + if (!_daw_out->connected()) { + AudioEngine::instance()->connect (_daw_out->name(), *po); + } +} + +int +LaunchPadX::ports_acquire () +{ + int ret = MIDISurface::ports_acquire (); + + if (!ret) { + _daw_in = AudioEngine::instance()->register_input_port (DataType::MIDI, string_compose (X_("%1 daw in"), port_name_prefix), true); + if (_daw_in) { + _daw_in_port = std::dynamic_pointer_cast(_daw_in).get(); + _daw_out = AudioEngine::instance()->register_output_port (DataType::MIDI, string_compose (X_("%1 daw out"), port_name_prefix), true); + } + if (_daw_out) { + _daw_out_port = std::dynamic_pointer_cast(_daw_out).get(); + return 0; + } + + ret = -1; + } + + return ret; +} + +void +LaunchPadX::ports_release () +{ + /* wait for button data to be flushed */ + MIDI::Port* daw_port = std::dynamic_pointer_cast(_daw_out).get(); + AsyncMIDIPort* asp; + asp = dynamic_cast (daw_port); + asp->drain (10000, 500000); + + { + Glib::Threads::Mutex::Lock em (AudioEngine::instance()->process_lock()); + AudioEngine::instance()->unregister_port (_daw_in); + AudioEngine::instance()->unregister_port (_daw_out); + } + + _daw_in.reset ((ARDOUR::Port*) 0); + _daw_out.reset ((ARDOUR::Port*) 0); + + MIDISurface::ports_release (); +} + +void +LaunchPadX::daw_write (const MidiByteArray& data) +{ + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("daw write %1 %2\n", data.size(), data)); + _daw_out_port->write (&data[0], data.size(), 0); +} + +void +LaunchPadX::daw_write (MIDI::byte const * data, size_t size) +{ + +#ifndef NDEBUG + std::stringstream str; + + if (DEBUG_ENABLED(DEBUG::Launchpad)) { + str << hex; + for (size_t n = 0; n < size; ++n) { + str << (int) data[n] << ' '; + } + } +#endif + + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("daw write %1 [%2]\n", size, str.str())); + _daw_out_port->write (data, size, 0); +} + +void +LaunchPadX::scroll_text (std::string const & txt, int color, bool loop, float speed) +{ + MidiByteArray msg (sysex_header); + + msg.push_back (0x32); + msg.push_back (color); + msg.push_back (loop ? 1: 0); + + for (std::string::size_type i = 0; i < txt.size(); ++i) { + msg.push_back (txt[i] & 0xf7); + } + + msg.push_back (0xf7); + daw_write (msg); + + if (speed != 0.f) { + msg[sysex_header.size() + 3] = (MIDI::byte) (floor (1.f + (speed * 6.f))); + msg[sysex_header.size() + 4] = 0xf7; + msg.resize (sysex_header.size() + 5); + daw_write (msg); + } +} + +LaunchPadX::StripableSlot +LaunchPadX::get_stripable_slot (int x, int y) const +{ + x += scroll_x_offset; + y += scroll_y_offset; + + if ((StripableSlotColumn::size_type) x > stripable_slots.size()) { + return StripableSlot (-1, -1); + } + + if ((StripableSlotRow::size_type) y > stripable_slots[x].size()) { + return StripableSlot (-1, -1); + } + + return stripable_slots[x][y]; +} + +void +LaunchPadX::stripable_selection_changed () +{ + std::shared_ptr pad_port = std::dynamic_pointer_cast(_async_in)->shadow_port(); + std::shared_ptr current_midi_track = _current_pad_target.lock(); + std::shared_ptr new_pad_target; + StripableNotificationList const & selected (last_selected()); + + if (_current_layout == Fader) { + map_faders (); + } + + std::shared_ptr first_selected; + + if (!selected.empty()) { + first_selected = selected.front().lock(); + } + + /* Make selected selection button "pulse" */ + + int selected_pad = -1; + + if (first_selected && first_selected->presentation_info().order() >= (uint32_t) scroll_x_offset && first_selected->presentation_info().order() < (uint32_t) scroll_x_offset + 8) { + /* subtract 1 because Master always has order zero XXX does * it? */ + selected_pad = first_selected->presentation_info().order() - 1 - scroll_x_offset; + light_pad (PadID (Lower1 + selected_pad), find_closest_palette_color (first_selected->presentation_info().color()), 1); + } + + if (first_selected) { + MIDI::byte msg[3]; + msg[0] = 0x90; + msg[1] = Sends; + if (first_selected->send_name (0).empty()) { + msg[2] = 0x0; + } else { + msg[2] = 0x2; + } + daw_write (msg, 3); + } + + /* Make all other selection buttons static */ + + for (int n = 0; n < 8; ++n) { + std::shared_ptr r = session->get_remote_nth_route (scroll_x_offset + n); + if (r) { + if (selected_pad >= 0 && (r == first_selected)) { + continue; + } + light_pad (PadID (Lower1 + n), find_closest_palette_color (r->presentation_info().color())); + } else { + light_pad (PadID (Lower1 + n), 0); + } + } + + /* See if there's a MIDI track selected */ + + for (StripableNotificationList::const_iterator si = selected.begin(); si != selected.end(); ++si) { + + new_pad_target = std::dynamic_pointer_cast ((*si).lock()); + + if (new_pad_target) { + break; + } + } + + if (current_midi_track != new_pad_target) { + + /* disconnect from pad port, if appropriate */ + + if (current_midi_track && pad_port) { + + /* XXX this could possibly leave dangling MIDI notes. + * + * A general libardour fix is required. It isn't obvious + * how note resolution can be done unless disconnecting + * becomes "slow" (i.e. deferred for as long as it takes + * to resolve notes). + */ + current_midi_track->input()->disconnect (current_midi_track->input()->nth(0), pad_port->name(), this); + } + + /* now connect the pad port to this (newly) selected midi + * track, if indeed there is one. + */ + + if (new_pad_target && pad_port) { + new_pad_target->input()->connect (new_pad_target->input()->nth (0), pad_port->name(), this); + _current_pad_target = new_pad_target; + } + } + + +} + +bool +LaunchPadX::pad_filter (MidiBuffer& in, MidiBuffer& out) const +{ + /* This filter is called asynchronously from a realtime process + context. It must use atomics to check state, and must not block. + */ + + switch (_current_layout) { + case NoteLayout: + case ChordLayout: + break; + default: + return false; + } + + bool matched = false; + + for (MidiBuffer::iterator ev = in.begin(); ev != in.end(); ++ev) { + if ((*ev).is_note_on() || (*ev).is_note_off() || + (*ev).is_channel_pressure() || (*ev).is_poly_pressure()) { + out.push_back (*ev); + matched = true; + } + } + + return matched; +} + +void +LaunchPadX::start_press_timeout (Pad& pad) +{ + Glib::RefPtr timeout = Glib::TimeoutSource::create (500); // milliseconds + pad.timeout_connection = timeout->connect (sigc::bind (sigc::mem_fun (*this, &LaunchPadX::long_press_timeout), pad.id)); + timeout->attach (main_loop()->get_context()); +} + +void +LaunchPadX::maybe_start_press_timeout (Pad& pad) +{ + if (pad.on_long_press == &LaunchPadX::relax) { + return; + } + start_press_timeout (pad); +} + +bool +LaunchPadX::long_press_timeout (int pad_id) +{ + PadMap::iterator p = pad_map.find (pad_id); + if (p == pad_map.end()) { + /* impossible */ + return false; + } + Pad& pad (p->second); + + (this->*pad.on_long_press) (pad); + + return false; /* don't get called again */ +} + +void +LaunchPadX::shift_press (Pad& pad) +{ + _shift_pressed = true; +} + +void +LaunchPadX::shift_release (Pad& pad) +{ + _shift_pressed = false; +} + +void +LaunchPadX::left_press (Pad& pad) +{ + const int shift = (_session_pressed ? 9 : 1); + if (scroll_x_offset >= shift) { + scroll_x_offset -= shift; + } + viewport_changed (); +} + +void +LaunchPadX::right_press (Pad& pad) +{ + const int shift = (_session_pressed ? 9 : 1); + scroll_x_offset += shift; + viewport_changed (); +} + +void +LaunchPadX::session_press (Pad& pad) +{ + if (_current_layout == SessionLayout) { + _session_pressed = true; + } +} + +void +LaunchPadX::session_release (Pad& pad) +{ + if (_current_layout == SessionLayout) { + _session_pressed = false; + } +} + +void +LaunchPadX::note_press (Pad& pad) +{ + /* handled by device */ +} + +void +LaunchPadX::chord_press (Pad& pad) +{ + /* handled by device */ +} + +void +LaunchPadX::custom_press (Pad& pad) +{ + /* handled by device */ +} + +void +LaunchPadX::sequencer_press (Pad& pad) +{ + /* handled by device */ +} + +void +LaunchPadX::projects_press (Pad& pad) +{ + /* handled by device */ +} + +void +LaunchPadX::cue_press (Pad& pad, int row) +{ + if (_clear_pressed) { + session->clear_cue (row); + } else { + session->trigger_cue_row (row); + } +} + +void +LaunchPadX::patterns_press (Pad& pad) +{ + if (_current_layout == SessionLayout) { + cue_press (pad, 0 + scroll_y_offset); + } +} + +void +LaunchPadX::steps_press (Pad& pad) +{ + if (_current_layout == SessionLayout) { + cue_press (pad, 1 + scroll_y_offset); + } +} + +void +LaunchPadX::pattern_settings_press (Pad& pad) +{ + if (_current_layout == SessionLayout) { + cue_press (pad, 2 + scroll_y_offset); + } +} + +void +LaunchPadX::velocity_press (Pad& pad) +{ + if (_current_layout == SessionLayout) { + cue_press (pad, 3 + scroll_y_offset); + } +} + +void +LaunchPadX::probability_press (Pad& pad) +{ + if (_current_layout == SessionLayout) { + cue_press (pad, 4 + scroll_y_offset); + } +} + +void +LaunchPadX::mutation_press (Pad& pad) +{ + if (_current_layout == SessionLayout) { + cue_press (pad, 5 + scroll_y_offset); + } +} + +void +LaunchPadX::microstep_press (Pad& pad) +{ + if (_current_layout == SessionLayout) { + cue_press (pad, 6 + scroll_y_offset); + } +} + +void +LaunchPadX::print_to_clip_press (Pad& pad) +{ + if (_current_layout == SessionLayout) { + cue_press (pad, 7 + scroll_y_offset); + } +} + +void +LaunchPadX::stop_clip_press (Pad& pad) +{ + session->trigger_stop_all (_shift_pressed); +} + +void +LaunchPadX::fader_long_press (Pad&) +{ + revert_layout_on_fader_release = true; +} + +void +LaunchPadX::fader_release (Pad&) +{ + if (revert_layout_on_fader_release) { + set_layout (pre_fader_layout); + revert_layout_on_fader_release = false; + } +} + +void +LaunchPadX::device_press (Pad& pad) +{ + if (_current_layout == Fader && current_fader_bank == DeviceFaders) { + set_layout (SessionLayout); + return; + } + set_layout (Fader, DeviceFaders); +} + +void +LaunchPadX::sends_press (Pad& pad) +{ + if (_current_layout == Fader && current_fader_bank == SendFaders) { + set_layout (SessionLayout); + return; + } + set_layout (Fader, SendFaders); +} + +void +LaunchPadX::pan_press (Pad& pad) +{ + if (_current_layout == Fader && current_fader_bank == PanFaders) { + set_layout (SessionLayout); + return; + } + set_layout (Fader, PanFaders); +} + +void +LaunchPadX::volume_press (Pad& pad) +{ + if (_current_layout == Fader && current_fader_bank == VolumeFaders) { + set_layout (SessionLayout); + return; + } + set_layout (Fader, VolumeFaders); +} + +void +LaunchPadX::solo_press (Pad& pad) +{ + if (_shift_pressed) { + toggle_click (); + return; + } + + std::shared_ptr s = session->selection().first_selected_stripable(); + if (s) { + std::shared_ptr ac = s->solo_control(); + if (ac) { + session->set_control (ac, !ac->get_value(), PBD::Controllable::UseGroup); + } + } +} + +void +LaunchPadX::solo_long_press (Pad& pad) +{ + cancel_all_solo (); + /* Pad was used for long press, do not invoke release action */ + consumed.insert (pad.id); +} + +void +LaunchPadX::mute_press (Pad& pad) +{ + if (_shift_pressed) { + redo (); + return; + } + + std::shared_ptr s = session->selection().first_selected_stripable(); + if (s) { + std::shared_ptr ac = s->mute_control(); + if (ac) { + ac->set_value (!ac->get_value(), PBD::Controllable::UseGroup); + } + } +} + +void +LaunchPadX::record_arm_press (Pad& pad) +{ + if (_shift_pressed) { + undo (); + return; + } + + std::shared_ptr s = session->selection().first_selected_stripable(); + if (s) { + std::shared_ptr ac = s->rec_enable_control(); + if (ac) { + ac->set_value (!ac->get_value(), PBD::Controllable::UseGroup); + } + } +} + +void +LaunchPadX::capture_midi_press (Pad& pad) +{ + set_record_enable (!get_record_enabled()); +} + +void +LaunchPadX::play_press (Pad& pad) +{ + toggle_roll (false, true); +} + +void +LaunchPadX::fixed_length_press (Pad& pad) +{ +} + +void +LaunchPadX::quantize_press (Pad& pad) +{ +} + +void +LaunchPadX::duplicate_press (Pad& pad) +{ +} + +void +LaunchPadX::clear_press (Pad& pad) +{ + _clear_pressed = true; +} + +void +LaunchPadX::clear_release (Pad& pad) +{ + _clear_pressed = false; +} + +void +LaunchPadX::down_press (Pad& pad) +{ + const int shift = (_session_pressed ? 9 : 1); + + if (scroll_y_offset >= shift) { + scroll_y_offset -= shift; + } +} + +void +LaunchPadX::up_press (Pad& pad) +{ + const int shift = (_session_pressed ? 9 : 1); + scroll_y_offset += shift; +} + +void +LaunchPadX::select_stripable (int n) +{ + if (_shift_pressed) { + session->selection().clear_stripables (); + return; + } + + std::shared_ptr r = session->get_remote_nth_route (scroll_x_offset + n); + if (r) { + session->selection().set (r, std::shared_ptr()); + } +} + +void +LaunchPadX::lower1_press (Pad& pad) +{ + select_stripable (0); +} + +void +LaunchPadX::lower2_press (Pad& pad) +{ + select_stripable (1); +} + +void +LaunchPadX::lower3_press (Pad& pad) +{ + select_stripable (2); +} + +void +LaunchPadX::lower4_press (Pad& pad) +{ + select_stripable (3); +} + +void +LaunchPadX::lower5_press (Pad& pad) +{ + select_stripable (4); +} + +void +LaunchPadX::lower6_press (Pad& pad) +{ + select_stripable (5); +} + +void +LaunchPadX::lower7_press (Pad& pad) +{ + select_stripable (6); +} + +void +LaunchPadX::lower8_press (Pad& pad) +{ + select_stripable (7); +} + +void +LaunchPadX::pad_press (Pad& pad, int velocity) +{ + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("pad press on %1, %2 => %3 vel %4\n", pad.x, pad.y, pad.id, velocity)); + + if (_clear_pressed) { + TriggerPtr tp = session->trigger_at (pad.x, pad.y); + if (tp) { + tp->set_region (std::shared_ptr()); + } + return; + } + + session->bang_trigger_at (pad.x, pad.y, velocity / 127.0f); + start_press_timeout (pad); +} + +void +LaunchPadX::pad_long_press (Pad& pad) +{ + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("pad long press on %1, %2 => %3\n", pad.x, pad.y, pad.id)); + session->unbang_trigger_at (pad.x, pad.y); + /* Pad was used for long press, do not invoke release action */ + consumed.insert (pad.id); +} + +void +LaunchPadX::trigger_property_change (PropertyChange pc, Trigger* t) +{ + int x = t->box().order(); + int y = t->index(); + + DEBUG_TRACE (DEBUG::Launchpad, string_compose ("prop change %1 for trigger at %2, %3\n", pc, x, y)); + + if (y > scroll_y_offset + 7) { + /* not visible at present */ + return; + } + + if (x > scroll_x_offset + 7) { + /* not visible at present */ + return; + } + + /* name property change is sent when slots are loaded or unloaded */ + + PropertyChange our_interests; + our_interests.add (Properties::running); + our_interests.add (Properties::name);; + + if (pc.contains (our_interests)) { + + int pid = (11 + ((7 - y) * 10)) + x; + MidiByteArray msg; + std::shared_ptr r = session->get_remote_nth_route (scroll_x_offset + x); + + if (!r || !t->region()) { + msg.push_back (0x90); + msg.push_back (pid); + msg.push_back (0x0); + daw_write (msg); + return; + } + + switch (t->state()) { + case Trigger::Stopped: + msg.push_back (0x90); + msg.push_back (pid); + msg.push_back (find_closest_palette_color (r->presentation_info().color())); + break; + + case Trigger::WaitingToStart: + msg.push_back (0x91); /* channel 1=> pulsing */ + msg.push_back (pid); + msg.push_back (0x17); // find_closest_palette_color (r->presentation_info().color())); + break; + + case Trigger::Running: + /* choose contrasting color from the base one */ + msg.push_back (0x90); + msg.push_back (pid); + msg.push_back (find_closest_palette_color (HSV(r->presentation_info().color()).opposite())); + break; + + case Trigger::WaitingForRetrigger: + case Trigger::WaitingToStop: + case Trigger::WaitingToSwitch: + case Trigger::Stopping: + msg.push_back (0x91); + msg.push_back (pid); + msg.push_back (find_closest_palette_color (HSV(r->presentation_info().color()).opposite())); + } + + daw_write (msg); + } +} + +void +LaunchPadX::map_triggers () +{ + for (int x = 0; x < 8; ++x) { + map_triggerbox (x); + } +} + +void +LaunchPadX::map_triggerbox (int x) +{ + MIDI::byte msg[3]; + + msg[0] = 0x90; + + std::shared_ptr r = session->get_remote_nth_route (scroll_x_offset + x); + int palette_index; + + if (r) { + palette_index = find_closest_palette_color (r->presentation_info().color()); + } else { + palette_index = 0x0; + } + + for (int y = 0; y < 8; ++y) { + + int xp = x + scroll_x_offset; + int yp = y + scroll_y_offset; + + int pid = (11 + ((7 - y) * 10)) + x; + msg[1] = pid; + + TriggerPtr t = session->trigger_at (xp, yp); + + if (!t || !t->region()) { + msg[2] = 0x0; + } else { + msg[2] = palette_index; + } + + daw_write (msg, 3); + } +} + +void +LaunchPadX::build_color_map () +{ + /* RGB values taken from using color picker on PDF of LP manual, page + * 10, but without zero (off) + */ + + static uint32_t novation_color_chart_left_side[] = { + 0xb3b3b3ff, + 0xddddddff, + 0xffffffff, + 0xffb3b3ff, + 0xff6161ff, + 0xdd6161ff, + 0xb36161ff, + 0xfff3d5ff, + 0xffb361ff, + 0xdd8c61ff, + 0xb37661ff, + 0xffeea1ff, + 0xffff61ff, + 0xdddd61ff, + 0xb3b361ff, + 0xddffa1ff, + 0xc2ff61ff, + 0xa1dd61ff, + 0x81b361ff, + 0xc2ffb3ff, + 0x61ff61ff, + 0x61dd61ff, + 0x61b361ff, + 0xc2ffc2ff, + 0x61ff8cff, + 0x61dd76ff, + 0x61b36bff, + 0xc2ffccff, + 0x61ffccff, + 0x61dda1ff, + 0x61b381ff, + 0xc2fff3ff, + 0x61ffe9ff, + 0x61ddc2ff, + 0x61b396ff, + 0xc2f3ffff, + 0x61eeffff, + 0x61c7ddff, + 0x61a1b3ff, + 0xc2ddffff, + 0x61c7ffff, + 0x61a1ddff, + 0x6181b3ff, + 0xa18cffff, + 0x6161ffff, + 0x6161ddff, + 0x6161b3ff, + 0xccb3ffff, + 0xa161ffff, + 0x8161ddff, + 0x7661b3ff, + 0xffb3ffff, + 0xff61ffff, + 0xdd61ddff, + 0xb361b3ff, + 0xffb3d5ff, + 0xff61c2ff, + 0xdd61a1ff, + 0xb3618cff, + 0xff7661ff, + 0xe9b361ff, + 0xddc261ff, + 0xa1a161ff, + }; + + static uint32_t novation_color_chart_right_side[] = { + 0x61b361ff, + 0x61b38cff, + 0x618cd5ff, + 0x6161ffff, + 0x61b3b3ff, + 0x8c61f3ff, + 0xccb3c2ff, + 0x8c7681ff, + /**/ + 0xff6161ff, + 0xf3ffa1ff, + 0xeefc61ff, + 0xccff61ff, + 0x76dd61ff, + 0x61ffccff, + 0x61e9ffff, + 0x61a1ffff, + /**/ + 0x8c61ffff, + 0xcc61fcff, + 0xcc61fcff, + 0xa17661ff, + 0xffa161ff, + 0xddf961ff, + 0xd5ff8cff, + 0x61ff61ff, + /**/ + 0xb3ffa1ff, + 0xccfcd5ff, + 0xb3fff6ff, + 0xcce4ffff, + 0xa1c2f6ff, + 0xd5c2f9ff, + 0xf98cffff, + 0xff61ccff, + /**/ + 0xff61ccff, + 0xf3ee61ff, + 0xe4ff61ff, + 0xddcc61ff, + 0xb3a161ff, + 0x61ba76ff, + 0x76c28cff, + 0x8181a1ff, + /**/ + 0x818cccff, + 0xccaa81ff, + 0xdd6161ff, + 0xf9b3a1ff, + 0xf9ba76ff, + 0xfff38cff, + 0xe9f9a1ff, + 0xd5ee76ff, + /**/ + 0x8181a1ff, + 0xf9f9d5ff, + 0xddfce4ff, + 0xe9e9ffff, + 0xe4d5ffff, + 0xb3b3b3ff, + 0xd5d5d5ff, + 0xf9ffffff, + /**/ + 0xe96161ff, + 0xe96161ff, + 0x81f661ff, + 0x61b361ff, + 0xf3ee61ff, + 0xb3a161ff, + 0xeec261ff, + 0xc27661ff + }; + + for (size_t n = 0; n < sizeof (novation_color_chart_left_side) / sizeof (novation_color_chart_left_side[0]); ++n) { + uint32_t color = novation_color_chart_left_side[n]; + /* Add 1 to account for missing zero at zero in the table */ + std::pair p (1 + n, color); + color_map.insert (p); + } + + for (size_t n = 0; n < sizeof (novation_color_chart_right_side) / sizeof (novation_color_chart_right_side[0]); ++n) { + uint32_t color = novation_color_chart_right_side[n]; + /* Add 40 to account for start offset number shown in page 10 of the LP manual */ + std::pair p (40 + n, color); + color_map.insert (p); + } +} + +int +LaunchPadX::find_closest_palette_color (uint32_t color) +{ + auto distance = std::numeric_limits::max(); + int index = -1; + + NearestMap::iterator n = nearest_map.find (color); + if (n != nearest_map.end()) { + return n->second; + } + + HSV hsv_c (color); + + for (auto const & c : color_map) { + + HSV hsv_p (c.second); + + double chr = M_PI * (hsv_c.h / 180.0); + double phr = M_PI * (hsv_p.h /180.0); + double t1 = (sin (chr) * hsv_c.s * hsv_c.v) - (sin (phr) * hsv_p.s* hsv_p.v); + double t2 = (cos (chr) * hsv_c.s * hsv_c.v) - (cos (phr) * hsv_p.s * hsv_p.v); + double t3 = hsv_c.v - hsv_p.v; + double d = (t1 * t1) + (t2 * t2) + (0.5 * (t3 * t3)); + + + if (d < distance) { + index = c.first; + distance = d; + } + } + + nearest_map.insert (std::pair (color, index)); + + return index; +} + +void +LaunchPadX::viewport_changed () +{ + route_connections.drop_connections (); + + for (int n = 0; n < 8; ++n) { + std::shared_ptr r = session->get_remote_nth_route (scroll_x_offset + n); + if (r) { + r->DropReferences.connect (route_connections, invalidator (*this), boost::bind (&LaunchPadX::viewport_changed, this), this); + r->presentation_info().PropertyChanged.connect (route_connections, invalidator (*this), boost::bind (&LaunchPadX::route_property_change, this, _1, n), this); + } else { + if (n == 0) { + /* not even the first stripable ... so do nothing */ + } + } + } + + + switch (_current_layout) { + case SessionLayout: + map_triggers (); + break; + case Fader: + map_faders (); + break; + default: + break; + } + + stripable_selection_changed (); +} + +void +LaunchPadX::route_property_change (PropertyChange const & pc, int col) +{ + if (pc.contains (Properties::color)) { + map_triggerbox (col); + } + + + if (pc.contains (Properties::selected)) { + } +} + +void +LaunchPadX::setup_faders (FaderBank bank) +{ + MidiByteArray msg (sysex_header); + + msg.push_back (1); /* fader bank command */ + msg.push_back (bank); + switch (bank) { + case PanFaders: + msg.push_back (1); /* vertical orientation */ + break; + default: + msg.push_back (0); /* vertical orientation */ + break; + } + for (int n = 0; n < 8; ++n) { + msg.push_back (n); /* fader number */ + switch (bank) { + case PanFaders: + msg.push_back (1); /* bipolar */ + break; + default: + msg.push_back (0); /* unipolar */ + break; + } + msg.push_back (0x20+n); /* CC number */ + msg.push_back (random() % 127); /* color */ + } + + msg.push_back (0xf7); + daw_write (msg); +} + +void +LaunchPadX::fader_move (int cc, int val) +{ + std::shared_ptr r; + + switch (current_fader_bank) { + case SendFaders: + case DeviceFaders: + r = std::dynamic_pointer_cast (session->selection().first_selected_stripable()); + break; + default: + r = session->get_remote_nth_route (scroll_x_offset + (cc - 0x20)); + break; + } + + if (!r) { + return; + } + + std::shared_ptr ac; + + switch (current_fader_bank) { + case VolumeFaders: + ac = r->gain_control(); + if (ac) { + session->set_control (ac, ARDOUR::slider_position_to_gain_with_max (val/127.0, ARDOUR::Config->get_max_gain()), PBD::Controllable::NoGroup); + } + break; + case PanFaders: + ac = r->pan_azimuth_control(); + if (ac) { + session->set_control (ac, val/127.0, PBD::Controllable::NoGroup); + } + break; + case SendFaders: + ac = r->send_level_controllable (scroll_x_offset + (cc - 0x20)); + if (ac) { + session->set_control (ac, ARDOUR::slider_position_to_gain_with_max (val/127.0, ARDOUR::Config->get_max_gain()), PBD::Controllable::NoGroup); + } + break; + default: + break; + } +} + +void +LaunchPadX::map_faders () +{ + MIDI::byte msg[3]; + msg[0] = 0xb4; + + control_connections.drop_connections (); + + for (int n = 0; n < 8; ++n) { + std::shared_ptr r; + + + switch (current_fader_bank) { + case SendFaders: + case DeviceFaders: + r = std::dynamic_pointer_cast (session->selection().first_selected_stripable()); + break; + default: + r = session->get_remote_nth_route (scroll_x_offset + n); + break; + } + + std::shared_ptr ac; + + msg[1] = 0x20 + n; + + if (!r) { + switch (current_fader_bank) { + case PanFaders: + msg[2] = 63; /* neutral position is halfway across */ + break; + default: + msg[2] = 0; /* neutral position is at bottom */ + break; + } + daw_write (msg, 3); + continue; + } + + switch (current_fader_bank) { + case VolumeFaders: + ac = r->gain_control(); + if (ac) { + msg[2] = (MIDI::byte) (ARDOUR::gain_to_slider_position_with_max (ac->get_value(), ARDOUR::Config->get_max_gain()) * 127.0); + } else { + msg[2] = 0; + } + break; + case PanFaders: + ac = r->pan_azimuth_control (); + if (ac) { + msg[2] = (MIDI::byte) (ac->get_value() * 127.0); + } else { + msg[2] = 0; + } + break; + case SendFaders: + ac = r->send_level_controllable (n); + if (ac) { + msg[2] = (MIDI::byte) (ARDOUR::gain_to_slider_position_with_max (ac->get_value(), ARDOUR::Config->get_max_gain()) * 127.0); + } else { + msg[2] = 0; + } + break; + default: + msg[2] = 0; + break; + } + + if (ac) { + ac->Changed.connect (control_connections, invalidator (*this), boost::bind (&LaunchPadX::automation_control_change, this, n, std::weak_ptr (ac)), this); + } + + daw_write (msg, 3); + } +} + +void +LaunchPadX::automation_control_change (int n, std::weak_ptr wac) +{ + std::shared_ptr ac = wac.lock(); + if (!ac) { + return; + } + + MIDI::byte msg[3]; + msg[0] = 0xb4; + msg[1] = 0x20 + n; + + switch (current_fader_bank) { + case VolumeFaders: + case SendFaders: + msg[2] = (MIDI::byte) (ARDOUR::gain_to_slider_position_with_max (ac->get_value(), ARDOUR::Config->get_max_gain()) * 127.0); + break; + case PanFaders: + msg[2] = (MIDI::byte) (ac->get_value() * 127.0); + break; + default: + break; + } + daw_write (msg, 3); +} diff --git a/libs/surfaces/launchpad_x/lpx.h b/libs/surfaces/launchpad_x/lpx.h new file mode 100644 index 0000000000..daa48230ba --- /dev/null +++ b/libs/surfaces/launchpad_x/lpx.h @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2016-2018 Paul Davis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __ardour_lpx_h__ +#define __ardour_lpx_h__ + +#include +#include +#include +#include +#include + +#include + +#define ABSTRACT_UI_EXPORTS +#include "pbd/abstract_ui.h" + +#include "midi++/types.h" + +#include "ardour/mode.h" +#include "ardour/types.h" + +#include "control_protocol/control_protocol.h" +#include "control_protocol/types.h" + +#include "gtkmm2ext/colors.h" + +#include "midi_surface/midi_byte_array.h" +#include "midi_surface/midi_surface.h" + +namespace MIDI { + class Parser; + class Port; +} + +namespace ARDOUR { + class AutomationControl; + class Port; + class MidiBuffer; + class MidiTrack; + class Trigger; +} + +namespace ArdourSurface { + +class LPX_GUI; + +class LaunchPadX : public MIDISurface +{ + public: + /* use hex for these constants, because we'll see them (as note numbers + and CC numbers) in hex within MIDI messages when debugging. + */ + enum PadID { + /* top */ + Shift = 0x5a, + Left = 0x5b, + Right = 0x5c, + Session = 0x5d, + Note = 0x5e, + Chord = 0x5f, + Custom = 0x60, + Sequencer = 0x61, + Projects = 0x62, + /* right side */ + Patterns = 0x59, + Steps = 0x4f, + PatternSettings = 0x45, + Velocity = 0x3b, + Probability = 0x31, + Mutation = 0x27, + MicroStep = 0x1d, + PrintToClip = 0x13, + /* lower bottom */ + StopClip = 0x8, + Device = 0x7, + Sends = 0x6, + Pan = 0x5, + Volume = 0x4, + Solo = 0x3, + Mute = 0x2, + RecordArm = 0x1, + /* left side */ + CaptureMIDI = 0xa, + Play = 0x14, + FixedLength = 0x1e, + Quantize = 0x28, + Duplicate = 0x32, + Clear = 0x3c, + Down = 0x46, + Up = 0x50, + /* upper bottom */ + Lower1 = 0x65, + Lower2 = 0x66, + Lower3 = 0x67, + Lower4 = 0x68, + Lower5 = 0x69, + Lower6 = 0x6a, + Lower7 = 0x6b, + Lower8 = 0x6c, + /* Logo */ + Logo = 0x63 + }; + + bool light_logo(); + void all_pads_out (); + + static const PadID all_pad_ids[]; + + LaunchPadX (ARDOUR::Session&); + ~LaunchPadX (); + + static bool available (); + static bool match_usb (uint16_t, uint16_t); + static bool probe (std::string&, std::string&); + + std::string input_port_name () const; + std::string output_port_name () const; + + bool has_editor () const { return true; } + void* get_gui () const; + void tear_down_gui (); + + int set_active (bool yn); + XMLNode& get_state() const; + int set_state (const XMLNode & node, int version); + + private: + enum DeviceMode { + Standalone, + DAW, + Programmer + }; + + enum Layout { + SessionLayout, + Fader, + ChordLayout, + CustomLayout, + NoteLayout, + Scale, + SequencerSettings, + SequencerSteps, + SequencerVelocity, + SequencerPatternSettings, + SequencerProbability, + SequencerMutation, + SequencerMicroStep, + SequencerProjects, + SequencerPatterns, + SequencerTempo, + SequencerSwing, + ProgrammerLayout, + Settings, + CustomSettings + }; + + enum FaderBank { + VolumeFaders, + PanFaders, + SendFaders, + DeviceFaders + }; + + static const Layout AllLayouts[]; + + struct Pad { + + enum ColorMode { + Static = 0x0, + Flashing = 0x1, + Pulsing = 0x2 + }; + + typedef void (LaunchPadX::*ButtonMethod)(Pad&); + typedef void (LaunchPadX::*PadMethod)(Pad&, int velocity); + + Pad (PadID pid, ButtonMethod press_method, ButtonMethod long_press_method = &LaunchPadX::relax, ButtonMethod release_method = &LaunchPadX::relax) + : id (pid) + , x (-1) + , y (-1) + { + on_press = press_method; + on_release = release_method; + on_long_press = long_press_method; + } + + Pad (int pid, int xx, int yy, PadMethod press_method, ButtonMethod long_press_method = &LaunchPadX::relax, ButtonMethod release_method = &LaunchPadX::relax) + : id (pid) + , x (xx) + , y (yy) + { + on_pad_press = press_method; + on_release = release_method; + on_long_press = long_press_method; + } + + MIDI::byte status_byte() const { if (x < 0) return 0xb0; return 0x90; } + bool is_pad () const { return x >= 0; } + bool is_button () const { return x < 0; } + + int id; + int x; + int y; + + /* It's either a button (CC number) or a pad (note number + * w/velocity info), never both. + */ + union { + ButtonMethod on_press; + PadMethod on_pad_press; + }; + + ButtonMethod on_release; + ButtonMethod on_long_press; + + sigc::connection timeout_connection; + }; + + void relax (Pad& p); + + std::set consumed; + + MIDI::byte logo_color; + + int scroll_x_offset; + int scroll_y_offset; + typedef std::pair StripableSlot; + typedef std::vector StripableSlotRow; + typedef std::vector StripableSlotColumn; + StripableSlotColumn stripable_slots; + + StripableSlot get_stripable_slot (int x, int y) const; + + typedef std::map PadMap; + PadMap pad_map; + void build_pad_map(); + Pad* pad_by_id (int pid); + + typedef std::map ColorMap; + ColorMap color_map; + void build_color_map (); + int find_closest_palette_color (uint32_t); + + typedef std::map NearestMap; + NearestMap nearest_map; + + int begin_using_device (); + int stop_using_device (); + int device_acquire () { return 0; } + void device_release () { } + void run_event_loop (); + void stop_event_loop (); + + void stripable_selection_changed (); + void select_stripable (int col); + std::weak_ptr _current_pad_target; + + void light_pad (int pad_id, int color, int mode = 0); + void pad_off (int pad_id); + void all_pads_off (); + void all_pads_on (int color); + + void set_device_mode (DeviceMode); + void set_layout (Layout, int page = 0); + + void handle_midi_controller_message (MIDI::Parser&, MIDI::EventTwoBytes*); + void handle_midi_note_on_message (MIDI::Parser&, MIDI::EventTwoBytes*); + void handle_midi_note_off_message (MIDI::Parser&, MIDI::EventTwoBytes*); + void handle_midi_sysex (MIDI::Parser&, MIDI::byte *, size_t count); + + MIDI::Port* _daw_in_port; + MIDI::Port* _daw_out_port; + std::shared_ptr _daw_in; + std::shared_ptr _daw_out; + + void port_registration_handler (); + int ports_acquire (); + void ports_release (); + void connect_daw_ports (); + + void daw_write (const MidiByteArray&); + void daw_write (MIDI::byte const *, size_t); + + void reconnect_for_programmer (); + void reconnect_for_session (); + + void scroll_text (std::string const &, int color, bool loop, float speed = 0); + + mutable LPX_GUI* _gui; + void build_gui (); + + Layout _current_layout; + + bool pad_filter (ARDOUR::MidiBuffer& in, ARDOUR::MidiBuffer& out) const; + + void maybe_start_press_timeout (Pad& pad); + void start_press_timeout (Pad& pad); + bool long_press_timeout (int pad_id); + + bool _shift_pressed; + bool _clear_pressed; + bool _duplicate_pressed; + bool _session_pressed; + + void cue_press (Pad&, int row); + + /* named pad methods */ + void shift_press (Pad&); + void shift_release (Pad&); + void shift_long_press (Pad&) {} + void left_press (Pad&); + void left_release (Pad&) {} + void left_long_press (Pad&) {} + void right_press (Pad&); + void right_release (Pad&) {} + void right_long_press (Pad&) {} + void session_press (Pad&); + void session_release (Pad&); + void session_long_press (Pad&) {} + void note_press (Pad&); + void note_release (Pad&) {} + void note_long_press (Pad&) {} + void chord_press (Pad&); + void chord_release (Pad&) {} + void chord_long_press (Pad&) {} + void custom_press (Pad&); + void custom_release (Pad&) {} + void custom_long_press (Pad&) {} + void sequencer_press (Pad&); + void sequencer_release (Pad&) {} + void sequencer_long_press (Pad&) {} + void projects_press (Pad&); + void projects_release (Pad&) {} + void projects_long_press (Pad&) {} + void patterns_press (Pad&); + void patterns_release (Pad&) {} + void patterns_long_press (Pad&) {} + void steps_press (Pad&); + void steps_release (Pad&) {} + void steps_long_press (Pad&) {} + void pattern_settings_press (Pad&); + void pattern_settings_release (Pad&) {} + void pattern_settings_long_press (Pad&) {} + void velocity_press (Pad&); + void velocity_release (Pad&) {} + void velocity_long_press (Pad&) {} + void probability_press (Pad&); + void probability_release (Pad&) {} + void probability_long_press (Pad&) {} + void mutation_press (Pad&); + void mutation_release (Pad&) {} + void mutation_long_press (Pad&) {} + void microstep_press (Pad&); + void microstep_release (Pad&) {} + void microstep_long_press (Pad&) {} + void print_to_clip_press (Pad&); + void print_to_clip_release (Pad&) {} + void print_to_clip_long_press (Pad&) {} + void stop_clip_press (Pad&); + void stop_clip_release (Pad&) {} + void stop_clip_long_press (Pad&) {} + void device_press (Pad&); + void device_release (Pad&) {} + void device_long_press (Pad&) {} + void sends_press (Pad&); + void sends_release (Pad&) {} + void sends_long_press (Pad&) {} + void pan_press (Pad&); + void pan_release (Pad&) {} + void pan_long_press (Pad&) {} + void volume_press (Pad&); + void volume_release (Pad&) {} + void volume_long_press (Pad&) {} + void solo_press (Pad&); + void solo_release (Pad&) {} + void solo_long_press (Pad&); + void mute_press (Pad&); + void mute_release (Pad&) {} + void mute_long_press (Pad&) {} + void record_arm_press (Pad&); + void record_arm_release (Pad&) {} + void record_arm_long_press (Pad&) {} + void capture_midi_press (Pad&); + void capture_midi_release (Pad&) {} + void capture_midi_long_press (Pad&) {} + void play_press (Pad&); + void play_release (Pad&) {} + void play_long_press (Pad&) {} + void fixed_length_press (Pad&); + void fixed_length_release (Pad&) {} + void fixed_length_long_press (Pad&) {} + void quantize_press (Pad&); + void quantize_release (Pad&) {} + void quantize_long_press (Pad&) {} + void duplicate_press (Pad&); + void duplicate_release (Pad&) {} + void duplicate_long_press (Pad&) {} + void clear_press (Pad&); + void clear_release (Pad&); + void clear_long_press (Pad&) {} + void down_press (Pad&); + void down_release (Pad&) {} + void down_long_press (Pad&) {} + void up_press (Pad&); + void up_release (Pad&) {} + void up_long_press (Pad&) {} + void lower1_press (Pad&); + void lower1_release (Pad&) {} + void lower1_long_press (Pad&) {} + void lower2_press (Pad&); + void lower2_release (Pad&) {} + void lower2_long_press (Pad&) {} + void lower3_press (Pad&); + void lower3_release (Pad&) {} + void lower3_long_press (Pad&) {} + void lower4_press (Pad&); + void lower4_release (Pad&) {} + void lower4_long_press (Pad&) {} + void lower5_press (Pad&); + void lower5_release (Pad&) {} + void lower5_long_press (Pad&) {} + void lower6_press (Pad&); + void lower6_release (Pad&) {} + void lower6_long_press (Pad&) {} + void lower7_press (Pad&); + void lower7_release (Pad&) {} + void lower7_long_press (Pad&) {} + void lower8_press (Pad&); + void lower8_release (Pad&) {} + void lower8_long_press (Pad&) {} + + void fader_long_press (Pad&); + void fader_release (Pad&); + + void pad_press (Pad&, int velocity); + void pad_long_press (Pad&); + + void trigger_property_change (PBD::PropertyChange, ARDOUR::Trigger*); + PBD::ScopedConnectionList trigger_connections; + + void display_session_layout (); + bool did_session_display; + void transport_state_changed (); + void record_state_changed (); + + void map_triggers (); + void map_triggerbox (int col); + + void viewport_changed (); + void route_property_change (PBD::PropertyChange const &, int x); + PBD::ScopedConnectionList route_connections; + + void setup_faders (FaderBank); + void map_faders (); + void fader_move (int cc, int val); + void automation_control_change (int n, std::weak_ptr); + PBD::ScopedConnectionList control_connections; + FaderBank current_fader_bank; + bool revert_layout_on_fader_release; + Layout pre_fader_layout; +}; + + +} /* namespace */ + +#endif /* __ardour_lpx_h__ */ diff --git a/libs/surfaces/launchpad_x/wscript b/libs/surfaces/launchpad_x/wscript new file mode 100644 index 0000000000..4f8bc3d094 --- /dev/null +++ b/libs/surfaces/launchpad_x/wscript @@ -0,0 +1,30 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import os + +def options(opt): + pass + +def configure(conf): + autowaf.check_pkg(conf, 'pangomm-1.4', uselib_store='PANGOMM', atleast_version='1.4', mandatory=True) + autowaf.check_pkg(conf, 'cairomm-1.0', uselib_store='CAIROMM', atleast_version='1.8.4', mandatory=True) + +def build(bld): + obj = bld(features = 'cxx cxxshlib') + obj.source = ''' + lpx.cc + interface.cc + gui.cc + ''' + obj.defines = [ 'PACKAGE="ardour_launchpad_x"' ] + obj.defines += [ 'ARDOURSURFACE_DLL_EXPORTS' ] + obj.defines += [ 'VERSIONSTRING="' + bld.env['VERSION'] + '"' ] + obj.includes = ['.', '..', './launchpad_x'] + obj.name = 'libardour_launchpad_x' + obj.target = 'ardour_launchpad_x' + obj.uselib = 'CAIROMM PANGOMM USB GTKMM SIGCPP XML OSX' + obj.use = 'libardour libardour_cp libardour_midisurface libgtkmm2ext libpbd libevoral libcanvas libtemporal' + obj.install_path = os.path.join(bld.env['LIBDIR'], 'surfaces') + +def shutdown(): + autowaf.shutdown()