529 lines
16 KiB
C++
529 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)
|
|
, _data_required (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 || _data_required) {
|
|
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;
|
|
}
|