reorganize push 2 code and logic to better handle device arrival after program startup
Note: we do not handle device departure correctly yet, mostly because the shadow (pad) port has a retained reference somewhere
This commit is contained in:
parent
8cff66c7e0
commit
1b830f2604
@ -71,7 +71,6 @@ Push2Canvas::vblank ()
|
||||
/* re-render dirty areas, if any */
|
||||
|
||||
if (expose ()) {
|
||||
DEBUG_TRACE (DEBUG::Push2, "re-blit to device frame buffer\n");
|
||||
/* something rendered, update device_frame_buffer */
|
||||
blit_to_device_frame_buffer ();
|
||||
|
||||
|
@ -76,6 +76,7 @@ Push2::Push2 (ARDOUR::Session& s)
|
||||
: ControlProtocol (s, string (X_("Ableton Push 2")))
|
||||
, AbstractUI<Push2Request> (name())
|
||||
, handle (0)
|
||||
, in_use (false)
|
||||
, _modifier_state (None)
|
||||
, splash_start (0)
|
||||
, _current_layout (0)
|
||||
@ -93,6 +94,9 @@ Push2::Push2 (ARDOUR::Session& s)
|
||||
, contrast_color (LED::Green)
|
||||
, in_range_select (false)
|
||||
{
|
||||
/* we're going to need this */
|
||||
|
||||
libusb_init (NULL);
|
||||
|
||||
build_maps ();
|
||||
build_color_map ();
|
||||
@ -101,17 +105,21 @@ Push2::Push2 (ARDOUR::Session& s)
|
||||
/* master cannot be removed, so no need to connect to going-away signal */
|
||||
master = session->master_out ();
|
||||
|
||||
if (open ()) {
|
||||
throw failed_constructor ();
|
||||
}
|
||||
|
||||
ControlProtocol::StripableSelectionChanged.connect (selection_connection, MISSING_INVALIDATOR, boost::bind (&Push2::stripable_selection_change, this, _1), this);
|
||||
|
||||
/* catch current selection, if any */
|
||||
{
|
||||
StripableNotificationListPtr sp (new StripableNotificationList (ControlProtocol::last_selected()));
|
||||
stripable_selection_change (sp);
|
||||
}
|
||||
/* allocate graphics layouts, even though we're not using them yet */
|
||||
|
||||
_canvas = new Push2Canvas (*this, 960, 160);
|
||||
mix_layout = new MixLayout (*this, *session, "globalmix");
|
||||
scale_layout = new ScaleLayout (*this, *session, "scale");
|
||||
track_mix_layout = new TrackMixLayout (*this, *session, "trackmix");
|
||||
splash_layout = new SplashLayout (*this, *session, "splash");
|
||||
|
||||
run_event_loop ();
|
||||
|
||||
/* Ports exist for the life of this instance */
|
||||
|
||||
ports_acquire ();
|
||||
|
||||
/* catch arrival and departure of Push2 itself */
|
||||
ARDOUR::AudioEngine::instance()->PortRegisteredOrUnregistered.connect (port_reg_connection, MISSING_INVALIDATOR, boost::bind (&Push2::port_registration_handler, this), this);
|
||||
@ -119,83 +127,103 @@ Push2::Push2 (ARDOUR::Session& s)
|
||||
/* Catch port connections and disconnections */
|
||||
ARDOUR::AudioEngine::instance()->PortConnectedOrDisconnected.connect (port_connection, MISSING_INVALIDATOR, boost::bind (&Push2::connection_handler, this, _1, _2, _3, _4, _5), this);
|
||||
|
||||
/* ports might already be there */
|
||||
/* Push 2 ports might already be there */
|
||||
port_registration_handler ();
|
||||
}
|
||||
|
||||
Push2::~Push2 ()
|
||||
{
|
||||
stop ();
|
||||
selection_connection.disconnect ();
|
||||
|
||||
stop_event_loop (); /* this will call stop_using_device () in Quit request handler */
|
||||
device_release ();
|
||||
ports_release ();
|
||||
|
||||
if (_current_layout) {
|
||||
_canvas->root()->remove (_current_layout);
|
||||
_current_layout = 0;
|
||||
}
|
||||
|
||||
delete track_mix_layout;
|
||||
delete mix_layout;
|
||||
mix_layout = 0;
|
||||
delete scale_layout;
|
||||
scale_layout = 0;
|
||||
delete splash_layout;
|
||||
splash_layout = 0;
|
||||
}
|
||||
|
||||
void
|
||||
Push2::port_registration_handler ()
|
||||
Push2::run_event_loop ()
|
||||
{
|
||||
if (!_async_in && !_async_out) {
|
||||
/* ports not registered yet */
|
||||
return;
|
||||
}
|
||||
DEBUG_TRACE (DEBUG::Push2, "start event loop\n");
|
||||
BaseUI::run ();
|
||||
}
|
||||
|
||||
if (_async_in->connected() && _async_out->connected()) {
|
||||
/* don't waste cycles here */
|
||||
return;
|
||||
}
|
||||
|
||||
string input_port_name = X_("Ableton Push 2 MIDI 1 in");
|
||||
string output_port_name = X_("Ableton Push 2 MIDI 1 out");
|
||||
vector<string> in;
|
||||
vector<string> out;
|
||||
|
||||
AudioEngine::instance()->get_ports (string_compose (".*%1", input_port_name), DataType::MIDI, PortFlags (IsPhysical|IsOutput), in);
|
||||
AudioEngine::instance()->get_ports (string_compose (".*%1", output_port_name), DataType::MIDI, PortFlags (IsPhysical|IsInput), out);
|
||||
|
||||
if (!in.empty() && !out.empty()) {
|
||||
cerr << "Push2: both ports found\n";
|
||||
cerr << "\tconnecting to " << in.front() << " + " << out.front() << endl;
|
||||
if (!_async_in->connected()) {
|
||||
AudioEngine::instance()->connect (_async_in->name(), in.front());
|
||||
}
|
||||
if (!_async_out->connected()) {
|
||||
AudioEngine::instance()->connect (_async_out->name(), out.front());
|
||||
}
|
||||
}
|
||||
void
|
||||
Push2::stop_event_loop ()
|
||||
{
|
||||
DEBUG_TRACE (DEBUG::Push2, "stop event loop\n");
|
||||
BaseUI::quit ();
|
||||
}
|
||||
|
||||
int
|
||||
Push2::open ()
|
||||
Push2::begin_using_device ()
|
||||
{
|
||||
int err;
|
||||
DEBUG_TRACE (DEBUG::Push2, "begin using device\n");
|
||||
|
||||
if (handle) {
|
||||
/* already open */
|
||||
/* set up periodic task used to push a frame buffer to the
|
||||
* device (25fps). The device can handle 60fps, but we don't
|
||||
* need that frame rate.
|
||||
*/
|
||||
|
||||
Glib::RefPtr<Glib::TimeoutSource> vblank_timeout = Glib::TimeoutSource::create (40); // milliseconds
|
||||
vblank_connection = vblank_timeout->connect (sigc::mem_fun (*this, &Push2::vblank));
|
||||
vblank_timeout->attach (main_loop()->get_context());
|
||||
|
||||
connect_session_signals ();
|
||||
|
||||
init_buttons (true);
|
||||
init_touch_strip ();
|
||||
set_pad_scale (_scale_root, _root_octave, _mode, _in_key);
|
||||
splash ();
|
||||
|
||||
/* catch current selection, if any so that we can wire up the pads if appropriate */
|
||||
{
|
||||
StripableNotificationListPtr sp (new StripableNotificationList (ControlProtocol::last_selected()));
|
||||
stripable_selection_change (sp);
|
||||
}
|
||||
|
||||
request_pressure_mode ();
|
||||
|
||||
in_use = true;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
Push2::stop_using_device ()
|
||||
{
|
||||
DEBUG_TRACE (DEBUG::Push2, "stop using device\n");
|
||||
|
||||
if (!in_use) {
|
||||
DEBUG_TRACE (DEBUG::Push2, "nothing to do, device not in use\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((handle = libusb_open_device_with_vid_pid (NULL, ABLETON, PUSH2)) == 0) {
|
||||
return -1;
|
||||
}
|
||||
init_buttons (false);
|
||||
strip_buttons_off ();
|
||||
|
||||
if ((err = libusb_claim_interface (handle, 0x00))) {
|
||||
return -1;
|
||||
}
|
||||
vblank_connection.disconnect ();
|
||||
session_connections.drop_connections ();
|
||||
|
||||
try {
|
||||
_canvas = new Push2Canvas (*this, 960, 160);
|
||||
mix_layout = new MixLayout (*this, *session, "globalmix");
|
||||
scale_layout = new ScaleLayout (*this, *session, "scale");
|
||||
track_mix_layout = new TrackMixLayout (*this, *session, "trackmix");
|
||||
splash_layout = new SplashLayout (*this, *session, "splash");
|
||||
} catch (...) {
|
||||
error << _("Cannot construct Canvas for display") << endmsg;
|
||||
libusb_release_interface (handle, 0x00);
|
||||
libusb_close (handle);
|
||||
handle = 0;
|
||||
return -1;
|
||||
}
|
||||
in_use = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
Push2::ports_acquire ()
|
||||
{
|
||||
DEBUG_TRACE (DEBUG::Push2, "acquiring ports\n");
|
||||
|
||||
/* setup ports */
|
||||
|
||||
@ -203,12 +231,13 @@ Push2::open ()
|
||||
_async_out = AudioEngine::instance()->register_output_port (DataType::MIDI, X_("Push 2 out"), true);
|
||||
|
||||
if (_async_in == 0 || _async_out == 0) {
|
||||
DEBUG_TRACE (DEBUG::Push2, "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.
|
||||
* really insist on that (and use JACK)
|
||||
*/
|
||||
|
||||
_input_port = boost::dynamic_pointer_cast<AsyncMIDIPort>(_async_in).get();
|
||||
@ -237,26 +266,21 @@ Push2::open ()
|
||||
|
||||
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, &Push2::midi_input_handler), _input_port));
|
||||
asp->xthread().attach (main_loop()->get_context());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
list<boost::shared_ptr<ARDOUR::Bundle> >
|
||||
Push2::bundles ()
|
||||
void
|
||||
Push2::ports_release ()
|
||||
{
|
||||
list<boost::shared_ptr<ARDOUR::Bundle> > b;
|
||||
|
||||
if (_output_bundle) {
|
||||
b.push_back (_output_bundle);
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
int
|
||||
Push2::close ()
|
||||
{
|
||||
init_buttons (false);
|
||||
strip_buttons_off ();
|
||||
DEBUG_TRACE (DEBUG::Push2, "releasing ports\n");
|
||||
|
||||
/* wait for button data to be flushed */
|
||||
AsyncMIDIPort* asp;
|
||||
@ -270,29 +294,57 @@ Push2::close ()
|
||||
_async_out.reset ((ARDOUR::Port*) 0);
|
||||
_input_port = 0;
|
||||
_output_port = 0;
|
||||
}
|
||||
|
||||
periodic_connection.disconnect ();
|
||||
session_connections.drop_connections ();
|
||||
int
|
||||
Push2::device_acquire ()
|
||||
{
|
||||
int err;
|
||||
|
||||
if (_current_layout) {
|
||||
_canvas->root()->remove (_current_layout);
|
||||
_current_layout = 0;
|
||||
DEBUG_TRACE (DEBUG::Push2, "acquiring device\n");
|
||||
|
||||
if (handle) {
|
||||
DEBUG_TRACE (DEBUG::Push2, "open() called with handle already set\n");
|
||||
/* already open */
|
||||
return 0;
|
||||
}
|
||||
|
||||
delete mix_layout;
|
||||
mix_layout = 0;
|
||||
delete scale_layout;
|
||||
scale_layout = 0;
|
||||
delete splash_layout;
|
||||
splash_layout = 0;
|
||||
if ((handle = libusb_open_device_with_vid_pid (NULL, ABLETON, PUSH2)) == 0) {
|
||||
DEBUG_TRACE (DEBUG::Push2, "failed to open USB handle\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ((err = libusb_claim_interface (handle, 0x00))) {
|
||||
DEBUG_TRACE (DEBUG::Push2, "failed to claim USB device\n");
|
||||
libusb_close (handle);
|
||||
handle = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
Push2::device_release ()
|
||||
{
|
||||
DEBUG_TRACE (DEBUG::Push2, "releasing device\n");
|
||||
if (handle) {
|
||||
libusb_release_interface (handle, 0x00);
|
||||
libusb_close (handle);
|
||||
handle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
list<boost::shared_ptr<ARDOUR::Bundle> >
|
||||
Push2::bundles ()
|
||||
{
|
||||
list<boost::shared_ptr<ARDOUR::Bundle> > b;
|
||||
|
||||
if (_output_bundle) {
|
||||
b.push_back (_output_bundle);
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
void
|
||||
@ -366,16 +418,6 @@ Push2::init_buttons (bool startup)
|
||||
bool
|
||||
Push2::probe ()
|
||||
{
|
||||
libusb_device_handle *h;
|
||||
libusb_init (NULL);
|
||||
|
||||
if ((h = libusb_open_device_with_vid_pid (NULL, ABLETON, PUSH2)) == 0) {
|
||||
DEBUG_TRACE (DEBUG::Push2, "no Push2 device found\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
libusb_close (h);
|
||||
DEBUG_TRACE (DEBUG::Push2, "Push2 device located\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -399,19 +441,10 @@ Push2::do_request (Push2Request * req)
|
||||
|
||||
} else if (req->type == Quit) {
|
||||
|
||||
stop ();
|
||||
stop_using_device ();
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
Push2::stop ()
|
||||
{
|
||||
BaseUI::quit ();
|
||||
close ();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Push2::splash ()
|
||||
{
|
||||
@ -454,49 +487,20 @@ Push2::set_active (bool yn)
|
||||
|
||||
if (yn) {
|
||||
|
||||
/* start event loop */
|
||||
|
||||
BaseUI::run ();
|
||||
|
||||
if (open ()) {
|
||||
DEBUG_TRACE (DEBUG::Push2, "device open failed\n");
|
||||
close ();
|
||||
if (device_acquire ()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 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, &Push2::midi_input_handler), _input_port));
|
||||
asp->xthread().attach (main_loop()->get_context());
|
||||
|
||||
connect_session_signals ();
|
||||
|
||||
/* set up periodic task used to push a frame buffer to the
|
||||
* device (25fps). The device can handle 60fps, but we don't
|
||||
* need that frame rate.
|
||||
*/
|
||||
|
||||
Glib::RefPtr<Glib::TimeoutSource> vblank_timeout = Glib::TimeoutSource::create (40); // milliseconds
|
||||
vblank_connection = vblank_timeout->connect (sigc::mem_fun (*this, &Push2::vblank));
|
||||
vblank_timeout->attach (main_loop()->get_context());
|
||||
|
||||
|
||||
Glib::RefPtr<Glib::TimeoutSource> periodic_timeout = Glib::TimeoutSource::create (1000); // milliseconds
|
||||
periodic_connection = periodic_timeout->connect (sigc::mem_fun (*this, &Push2::periodic));
|
||||
periodic_timeout->attach (main_loop()->get_context());
|
||||
|
||||
init_buttons (true);
|
||||
init_touch_strip ();
|
||||
set_pad_scale (_scale_root, _root_octave, _mode, _in_key);
|
||||
splash ();
|
||||
if ((connection_state & (InputConnected|OutputConnected)) == (InputConnected|OutputConnected)) {
|
||||
begin_using_device ();
|
||||
} else {
|
||||
/* begin_using_device () will get called once we're connected */
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
stop ();
|
||||
|
||||
/* Control Protocol Manager never calls us with false, but
|
||||
* insteads destroys us.
|
||||
*/
|
||||
}
|
||||
|
||||
ControlProtocol::set_active (yn);
|
||||
@ -537,27 +541,23 @@ Push2::midi_input_handler (IOCondition ioc, MIDI::Port* port)
|
||||
|
||||
if (ioc & IO_IN) {
|
||||
|
||||
// DEBUG_TRACE (DEBUG::Push2, string_compose ("something happend on %1\n", port->name()));
|
||||
DEBUG_TRACE (DEBUG::Push2, string_compose ("something happend on %1\n", port->name()));
|
||||
|
||||
AsyncMIDIPort* asp = dynamic_cast<AsyncMIDIPort*>(port);
|
||||
if (asp) {
|
||||
asp->clear ();
|
||||
}
|
||||
|
||||
//DEBUG_TRACE (DEBUG::Push2, string_compose ("data available on %1\n", port->name()));
|
||||
DEBUG_TRACE (DEBUG::Push2, string_compose ("data available on %1\n", port->name()));
|
||||
if (in_use) {
|
||||
framepos_t now = AudioEngine::instance()->sample_time();
|
||||
port->parse (now);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
Push2::periodic ()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
Push2::connect_to_parser ()
|
||||
{
|
||||
@ -1166,6 +1166,47 @@ Push2::pad_filter (MidiBuffer& in, MidiBuffer& out) const
|
||||
return matched;
|
||||
}
|
||||
|
||||
void
|
||||
Push2::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;
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
/* the origin of the numeric magic identifiers is known only to Ableton
|
||||
and may change in time. This is part of how CoreMIDI works.
|
||||
*/
|
||||
string input_port_name = X_("system:midi_capture_1319078870");
|
||||
string output_port_name = X_("system:midi_playback_3409210341");
|
||||
#else
|
||||
string input_port_name = X_("Ableton Push 2 MIDI 1 in");
|
||||
string output_port_name = X_("Ableton Push 2 MIDI 1 out");
|
||||
#endif
|
||||
vector<string> in;
|
||||
vector<string> out;
|
||||
|
||||
AudioEngine::instance()->get_ports (string_compose (".*%1", input_port_name), DataType::MIDI, PortFlags (IsPhysical|IsOutput), in);
|
||||
AudioEngine::instance()->get_ports (string_compose (".*%1", output_port_name), DataType::MIDI, PortFlags (IsPhysical|IsInput), out);
|
||||
|
||||
if (!in.empty() && !out.empty()) {
|
||||
cerr << "Push2: both ports found\n";
|
||||
cerr << "\tconnecting to " << in.front() << " + " << out.front() << endl;
|
||||
if (!_async_in->connected()) {
|
||||
AudioEngine::instance()->connect (_async_in->name(), in.front());
|
||||
}
|
||||
if (!_async_out->connected()) {
|
||||
AudioEngine::instance()->connect (_async_out->name(), out.front());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
Push2::connection_handler (boost::weak_ptr<ARDOUR::Port>, std::string name1, boost::weak_ptr<ARDOUR::Port>, std::string name2, bool yn)
|
||||
{
|
||||
@ -1190,11 +1231,14 @@ Push2::connection_handler (boost::weak_ptr<ARDOUR::Port>, std::string name1, boo
|
||||
connection_state &= ~OutputConnected;
|
||||
}
|
||||
} else {
|
||||
DEBUG_TRACE (DEBUG::FaderPort, string_compose ("Connections between %1 and %2 changed, but I ignored it\n", name1, name2));
|
||||
DEBUG_TRACE (DEBUG::Push2, string_compose ("Connections between %1 and %2 changed, but I ignored it\n", name1, name2));
|
||||
/* not our ports */
|
||||
return false;
|
||||
}
|
||||
|
||||
DEBUG_TRACE (DEBUG::Push2, string_compose ("our ports changed connection state: %1 -> %2 connected ? %3\n",
|
||||
name1, name2, yn));
|
||||
|
||||
if ((connection_state & (InputConnected|OutputConnected)) == (InputConnected|OutputConnected)) {
|
||||
|
||||
/* XXX this is a horrible hack. Without a short sleep here,
|
||||
@ -1203,11 +1247,19 @@ Push2::connection_handler (boost::weak_ptr<ARDOUR::Port>, std::string name1, boo
|
||||
*/
|
||||
|
||||
g_usleep (100000);
|
||||
DEBUG_TRACE (DEBUG::FaderPort, "device now connected for both input and output\n");
|
||||
connected ();
|
||||
DEBUG_TRACE (DEBUG::Push2, "device now connected for both input and output\n");
|
||||
|
||||
/* 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::FaderPort, "Device disconnected (input or output or both) or not yet fully connected\n");
|
||||
stop_using_device ();
|
||||
}
|
||||
|
||||
ConnectionChange (); /* emit signal for our GUI */
|
||||
@ -1217,12 +1269,6 @@ Push2::connection_handler (boost::weak_ptr<ARDOUR::Port>, std::string name1, boo
|
||||
return true; /* connection status changed */
|
||||
}
|
||||
|
||||
void
|
||||
Push2::connected ()
|
||||
{
|
||||
request_pressure_mode ();
|
||||
}
|
||||
|
||||
boost::shared_ptr<Port>
|
||||
Push2::output_port()
|
||||
{
|
||||
|
@ -370,12 +370,19 @@ class Push2 : public ARDOUR::ControlProtocol
|
||||
|
||||
private:
|
||||
libusb_device_handle *handle;
|
||||
bool in_use;
|
||||
ModifierState _modifier_state;
|
||||
|
||||
void do_request (Push2Request*);
|
||||
int stop ();
|
||||
int open ();
|
||||
int close ();
|
||||
|
||||
int begin_using_device ();
|
||||
int stop_using_device ();
|
||||
int device_acquire ();
|
||||
void device_release ();
|
||||
int ports_acquire ();
|
||||
void ports_release ();
|
||||
void run_event_loop ();
|
||||
void stop_event_loop ();
|
||||
|
||||
void relax () {}
|
||||
|
||||
@ -431,9 +438,6 @@ class Push2 : public ARDOUR::ControlProtocol
|
||||
|
||||
bool midi_input_handler (Glib::IOCondition ioc, MIDI::Port* port);
|
||||
|
||||
sigc::connection periodic_connection;
|
||||
bool periodic ();
|
||||
|
||||
void thread_init ();
|
||||
|
||||
PBD::ScopedConnectionList session_connections;
|
||||
|
Loading…
Reference in New Issue
Block a user