Paul Davis
e175410f54
1. do more to ensure that we do not call MidiSurface::begin_using_device() multiple times without ::stop_using_device() in between. This reduces the risk of duplicate signal handler connections being made (it might even eliminate it). 2. Notify all control surfaces when MIDI connectivity is established AND disestablished. This gives them a chance to update their notion of their current connection state. This can be important with JACK across zombification, but also likely across backend stop&start. These changes currntly only impact classes derived from MidiSurface but something equivalent is required for all control surfaces
528 lines
16 KiB
C++
528 lines
16 KiB
C++
/*
|
|
* Copyright (C) 2022 Robin Gareus <robin@gareus.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include <regex>
|
|
|
|
#include "pbd/debug.h"
|
|
#include "pbd/i18n.h"
|
|
|
|
#include "ardour/async_midi_port.h"
|
|
#include "ardour/audioengine.h"
|
|
#include "ardour/bundle.h"
|
|
#include "ardour/debug.h"
|
|
#include "ardour/midiport_manager.h"
|
|
#include "ardour/midi_port.h"
|
|
#include "ardour/session.h"
|
|
|
|
#include "midi_surface.h"
|
|
|
|
using namespace ARDOUR;
|
|
using namespace Glib;
|
|
using namespace PBD;
|
|
|
|
#include "pbd/abstract_ui.cc" // instantiate template
|
|
|
|
MIDISurface::MIDISurface (ARDOUR::Session& s, std::string const & namestr, std::string const & port_prefix, bool use_pad_filter)
|
|
: ControlProtocol (s, namestr)
|
|
, AbstractUI<MidiSurfaceRequest> (namestr)
|
|
, with_pad_filter (use_pad_filter)
|
|
, _in_use (false)
|
|
, port_name_prefix (port_prefix)
|
|
, _connection_state (ConnectionState (0))
|
|
{
|
|
}
|
|
|
|
MIDISurface::~MIDISurface ()
|
|
{
|
|
/* leave it all up to derived classes, because ordering it hard. */
|
|
}
|
|
|
|
void
|
|
MIDISurface::port_setup ()
|
|
{
|
|
ports_acquire ();
|
|
|
|
if (!input_port_name().empty() || !output_port_name().empty()) {
|
|
ARDOUR::AudioEngine::instance()->PortRegisteredOrUnregistered.connect (port_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::port_registration_handler, this), this);
|
|
}
|
|
ARDOUR::AudioEngine::instance()->PortConnectedOrDisconnected.connect (port_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::connection_handler, this, _1, _2, _3, _4, _5), this);
|
|
|
|
port_registration_handler ();
|
|
}
|
|
|
|
void
|
|
MIDISurface::drop ()
|
|
{
|
|
/* do this before stopping the event loop, so that we don't get any notifications */
|
|
port_connections.drop_connections ();
|
|
stop_using_device ();
|
|
device_release ();
|
|
ports_release ();
|
|
}
|
|
|
|
int
|
|
MIDISurface::ports_acquire ()
|
|
{
|
|
DEBUG_TRACE (DEBUG::MIDISurface, "acquiring ports\n");
|
|
|
|
/* setup ports */
|
|
|
|
_async_in = AudioEngine::instance()->register_input_port (DataType::MIDI, string_compose (X_("%1 in"), port_name_prefix), true);
|
|
_async_out = AudioEngine::instance()->register_output_port (DataType::MIDI, string_compose (X_("%1 out"), port_name_prefix), true);
|
|
|
|
if (_async_in == 0 || _async_out == 0) {
|
|
DEBUG_TRACE (DEBUG::MIDISurface, "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<AsyncMIDIPort>(_async_in).get();
|
|
_output_port = std::dynamic_pointer_cast<AsyncMIDIPort>(_async_out).get();
|
|
|
|
/* Create a shadow port where, depending on the state of the surface,
|
|
* we will make pad note on/off events appear. The surface code will
|
|
* automatically this port to the first selected MIDI track.
|
|
*/
|
|
|
|
if (with_pad_filter) {
|
|
std::dynamic_pointer_cast<AsyncMIDIPort>(_async_in)->add_shadow_port (string_compose (_("%1 Pads"), port_name_prefix), boost::bind (&MIDISurface::pad_filter, this, _1, _2));
|
|
std::shared_ptr<MidiPort> shadow_port = std::dynamic_pointer_cast<AsyncMIDIPort>(_async_in)->shadow_port();
|
|
|
|
if (shadow_port) {
|
|
|
|
_output_bundle.reset (new ARDOUR::Bundle (port_name_prefix, false));
|
|
|
|
_output_bundle->add_channel (
|
|
shadow_port->name(),
|
|
ARDOUR::DataType::MIDI,
|
|
session->engine().make_port_name_non_relative (shadow_port->name())
|
|
);
|
|
}
|
|
}
|
|
|
|
session->BundleAddedOrRemoved ();
|
|
|
|
connect_to_parser ();
|
|
|
|
/* Connect input port to event loop */
|
|
|
|
AsyncMIDIPort* asp;
|
|
|
|
asp = dynamic_cast<AsyncMIDIPort*> (_input_port);
|
|
asp->xthread().set_receive_handler (sigc::bind (sigc::mem_fun (this, &MIDISurface::midi_input_handler), _input_port));
|
|
asp->xthread().attach (main_loop()->get_context());
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
MIDISurface::ports_release ()
|
|
{
|
|
DEBUG_TRACE (DEBUG::MIDISurface, "releasing ports\n");
|
|
|
|
/* wait for button data to be flushed */
|
|
AsyncMIDIPort* asp;
|
|
asp = dynamic_cast<AsyncMIDIPort*> (_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;
|
|
}
|
|
|
|
void
|
|
MIDISurface::port_registration_handler ()
|
|
{
|
|
if (!_async_in || !_async_out) {
|
|
/* ports not registered yet */
|
|
return;
|
|
}
|
|
|
|
if (_async_in->connected() && _async_out->connected()) {
|
|
/* don't waste cycles here */
|
|
return;
|
|
}
|
|
|
|
std::vector<std::string> midi_inputs;
|
|
std::vector<std::string> midi_outputs;
|
|
|
|
AudioEngine::instance()->get_ports ("", DataType::MIDI, PortFlags (IsPhysical|IsOutput), midi_inputs);
|
|
AudioEngine::instance()->get_ports ("", DataType::MIDI, PortFlags (IsPhysical|IsInput), midi_outputs);
|
|
|
|
if (midi_inputs.empty() || midi_outputs.empty()) {
|
|
return;
|
|
}
|
|
|
|
/* Try to find the input & output ports, whose pretty name varies on
|
|
* Linux depending on the version of ALSA, but is fairly consistent
|
|
* across newer ALSA and other platforms.
|
|
*/
|
|
|
|
/* See if the input port is available, and maybe connect that */
|
|
|
|
string ip = input_port_name ();
|
|
|
|
if (ip[0] == ':') {
|
|
std::regex rx (ip.substr (1), std::regex::extended);
|
|
|
|
auto is_the_input = [&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_the_input);
|
|
if (pi != midi_inputs.end()) {
|
|
AudioEngine::instance()->connect (_async_in->name(), *pi);
|
|
}
|
|
} else {
|
|
/* regular partial string search */
|
|
auto is_the_input = [&ip](string const &s) {
|
|
std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s);
|
|
return pn.find (ip) != string::npos;
|
|
};
|
|
|
|
auto pi = std::find_if (midi_inputs.begin(), midi_inputs.end(), is_the_input);
|
|
if (pi != midi_inputs.end()) {
|
|
AudioEngine::instance()->connect (_async_in->name(), *pi);
|
|
}
|
|
}
|
|
|
|
/* Now see if the output port is available, and maybe connect that */
|
|
|
|
string op = output_port_name ();
|
|
|
|
if (op[0] == ':') {
|
|
std::regex rx (op.substr (1), std::regex::extended);
|
|
|
|
auto is_the_output = [&rx](string const &s) {
|
|
std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s);
|
|
return std::regex_search (pn, rx);
|
|
};
|
|
|
|
auto po = std::find_if (midi_outputs.begin(), midi_outputs.end(), is_the_output);
|
|
if (po != midi_outputs.end()) {
|
|
AudioEngine::instance()->connect (_async_in->name(), *po);
|
|
}
|
|
} else {
|
|
/* regular partial string search */
|
|
auto is_the_output = [&op](string const &s) {
|
|
std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s);
|
|
return pn.find (op) != string::npos;
|
|
};
|
|
|
|
auto po = std::find_if (midi_outputs.begin(), midi_outputs.end(), is_the_output);
|
|
if (po != midi_outputs.end()) {
|
|
AudioEngine::instance()->connect (_async_in->name(), *po);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
bool
|
|
MIDISurface::connection_handler (std::weak_ptr<ARDOUR::Port>, std::string name1, std::weak_ptr<ARDOUR::Port>, std::string name2, bool yn)
|
|
{
|
|
DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("MIDISurface::connection_handler start, %1 %2 %3\n", name1, (yn ? "connected" : "disconnected"), name2));
|
|
|
|
if (!_input_port || !_output_port) {
|
|
return false;
|
|
}
|
|
|
|
std::string ni = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (std::shared_ptr<ARDOUR::Port>(_async_in)->name());
|
|
std::string no = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (std::shared_ptr<ARDOUR::Port>(_async_out)->name());
|
|
|
|
int old_connection_state = _connection_state;
|
|
|
|
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::MIDISurface, string_compose ("Connections between %1 and %2 changed, but I ignored it\n", name1, name2));
|
|
/* not our ports */
|
|
return false;
|
|
}
|
|
|
|
DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("our ports changed connection state: %1 -> %2 connected ? %3, connection state now %4\n",
|
|
name1, name2, yn, _connection_state));
|
|
|
|
/* it ought o be impossible for the connection state of our ports to
|
|
* change without a corresponding change in _connection_state. But
|
|
* since the consequences of calling device_acquire() and
|
|
* begin_using_device() are substantial, include it as a test to catch
|
|
* any weird corner cases.
|
|
*/
|
|
|
|
if ((_connection_state != old_connection_state) && (_connection_state & (InputConnected|OutputConnected)) == (InputConnected|OutputConnected)) {
|
|
|
|
DEBUG_TRACE (DEBUG::MIDISurface, "device now connected for both input and output\n");
|
|
|
|
if (!_in_use) {
|
|
|
|
/* 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);
|
|
|
|
/* may not have the device open if it was just plugged
|
|
in. Really need USB device detection rather than MIDI port
|
|
detection for this to work well.
|
|
*/
|
|
|
|
device_acquire ();
|
|
begin_using_device ();
|
|
}
|
|
|
|
} else {
|
|
DEBUG_TRACE (DEBUG::MIDISurface, "Device disconnected (input or output or both) or not yet fully connected\n");
|
|
stop_using_device ();
|
|
}
|
|
|
|
ConnectionChange (); /* emit signal for our GUI */
|
|
|
|
DEBUG_TRACE (DEBUG::MIDISurface, "connection_handler end\n");
|
|
|
|
return true; /* connection status changed */
|
|
}
|
|
|
|
std::shared_ptr<Port>
|
|
MIDISurface::output_port()
|
|
{
|
|
return _async_out;
|
|
}
|
|
|
|
std::shared_ptr<Port>
|
|
MIDISurface::input_port()
|
|
{
|
|
return _async_in;
|
|
}
|
|
|
|
void
|
|
MIDISurface::write (const MidiByteArray& data)
|
|
{
|
|
/* immediate delivery */
|
|
_output_port->write (&data[0], data.size(), 0);
|
|
}
|
|
|
|
void
|
|
MIDISurface::write (MIDI::byte const * data, size_t size)
|
|
{
|
|
_output_port->write (data, size, 0);
|
|
}
|
|
|
|
void
|
|
MIDISurface::midi_connectivity_established (bool yn)
|
|
{
|
|
if (!yn) {
|
|
_connection_state = ConnectionState (0);
|
|
stop_using_device ();
|
|
}
|
|
}
|
|
|
|
bool
|
|
MIDISurface::midi_input_handler (IOCondition ioc, MIDI::Port* port)
|
|
{
|
|
if (ioc & ~IO_IN) {
|
|
DEBUG_TRACE (DEBUG::MIDISurface, "MIDI port closed\n");
|
|
return false;
|
|
}
|
|
|
|
if (ioc & IO_IN) {
|
|
|
|
DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("something happened on %1\n", port->name()));
|
|
|
|
AsyncMIDIPort* asp = dynamic_cast<AsyncMIDIPort*>(port);
|
|
if (asp) {
|
|
asp->clear ();
|
|
}
|
|
|
|
DEBUG_TRACE (DEBUG::MIDISurface, 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
|
|
MIDISurface::connect_to_parser ()
|
|
{
|
|
connect_to_port_parser (*_input_port);
|
|
}
|
|
|
|
void
|
|
MIDISurface::connect_to_port_parser (MIDI::Port& port)
|
|
{
|
|
MIDI::Parser* p = port.parser();
|
|
|
|
DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("Connecting to signals on port %1 using parser %2\n", port.name(), p));
|
|
|
|
/* Incoming sysex */
|
|
p->sysex.connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_sysex, this, _1, _2, _3));
|
|
/* V-Pot messages are Controller */
|
|
p->controller.connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_controller_message, this, _1, _2));
|
|
/* Button messages are NoteOn */
|
|
p->note_on.connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_note_on_message, this, _1, _2));
|
|
/* Button messages are NoteOn but libmidi++ sends note-on w/velocity = 0 as note-off so catch them too */
|
|
p->note_off.connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_note_off_message, this, _1, _2));
|
|
/* Fader messages are Pitchbend */
|
|
p->channel_pitchbend[0].connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_pitchbend_message, this, _1, _2));
|
|
|
|
p->poly_pressure.connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_polypressure_message, this, _1, _2));
|
|
}
|
|
|
|
void
|
|
MIDISurface::thread_init ()
|
|
{
|
|
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);
|
|
|
|
set_thread_priority ();
|
|
}
|
|
|
|
void
|
|
MIDISurface::connect_session_signals()
|
|
{
|
|
// receive routes added
|
|
//session->RouteAdded.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MackieControlProtocol::notify_routes_added, this, _1), this);
|
|
// receive VCAs added
|
|
//session->vca_manager().VCAAdded.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_vca_added, this, _1), this);
|
|
|
|
// receive record state toggled
|
|
session->RecordStateChanged.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_record_state_changed, this), this);
|
|
// receive transport state changed
|
|
session->TransportStateChange.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_transport_state_changed, this), this);
|
|
session->TransportLooped.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_loop_state_changed, this), this);
|
|
// receive punch-in and punch-out
|
|
Config->ParameterChanged.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_parameter_changed, this, _1), this);
|
|
session->config.ParameterChanged.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_parameter_changed, this, _1), this);
|
|
// receive rude solo changed
|
|
session->SoloActive.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_solo_active_changed, this, _1), this);
|
|
}
|
|
|
|
XMLNode&
|
|
MIDISurface::get_state() const
|
|
{
|
|
XMLNode& node (ControlProtocol::get_state());
|
|
XMLNode* child;
|
|
|
|
child = new XMLNode (X_("Input"));
|
|
child->add_child_nocopy (_async_in->get_state());
|
|
node.add_child_nocopy (*child);
|
|
child = new XMLNode (X_("Output"));
|
|
child->add_child_nocopy (_async_out->get_state());
|
|
node.add_child_nocopy (*child);
|
|
|
|
return node;
|
|
}
|
|
|
|
int
|
|
MIDISurface::set_state (const XMLNode & node, int version)
|
|
{
|
|
DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("MIDISurface::set_state: active %1\n", active()));
|
|
|
|
if (ControlProtocol::set_state (node, version)) {
|
|
return -1;
|
|
}
|
|
|
|
XMLNode* child;
|
|
|
|
if ((child = node.child (X_("Input"))) != 0) {
|
|
XMLNode* portnode = child->child (Port::state_node_name.c_str());
|
|
if (portnode) {
|
|
portnode->remove_property ("name");
|
|
_async_in->set_state (*portnode, version);
|
|
}
|
|
}
|
|
|
|
if ((child = node.child (X_("Output"))) != 0) {
|
|
XMLNode* portnode = child->child (Port::state_node_name.c_str());
|
|
if (portnode) {
|
|
portnode->remove_property ("name");
|
|
_async_out->set_state (*portnode, version);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
MIDISurface::do_request (MidiSurfaceRequest * req)
|
|
{
|
|
if (req->type == CallSlot) {
|
|
|
|
call_slot (PBD::EventLoop::__invalidator (*this, __FILE__, __LINE__), req->the_slot);
|
|
|
|
} else if (req->type == Quit) {
|
|
|
|
stop_using_device ();
|
|
}
|
|
}
|
|
|
|
int
|
|
MIDISurface::begin_using_device ()
|
|
{
|
|
_in_use = true;
|
|
connect_session_signals ();
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
MIDISurface::stop_using_device ()
|
|
{
|
|
session_connections.drop_connections ();
|
|
_in_use = false;
|
|
return 0;
|
|
}
|
|
|
|
std::list<std::shared_ptr<ARDOUR::Bundle> >
|
|
MIDISurface::bundles ()
|
|
{
|
|
std::list<std::shared_ptr<ARDOUR::Bundle> > b;
|
|
|
|
if (_output_bundle) {
|
|
b.push_back (_output_bundle);
|
|
}
|
|
|
|
return b;
|
|
}
|