Move event handling logic to a dedicated class

Now all pieces are in place to allow handling events over multiple
socket connections.
This commit is contained in:
Robbert van der Helm
2020-10-25 23:08:11 +01:00
parent 54ed69c408
commit 74c3cab046
8 changed files with 581 additions and 544 deletions
+21 -19
View File
@@ -107,25 +107,27 @@ as the _Windows VST plugin_. The whole process works as follows:
are located in `src/common/communication.h`. The actual binary serialization are located in `src/common/communication.h`. The actual binary serialization
is handled using [bitsery](https://github.com/fraillt/bitsery). is handled using [bitsery](https://github.com/fraillt/bitsery).
Actually sending and receiving the events happens in the `send_event()` and TODO: Rewrite this after the socket changes are done
`receive_event()` functions. When calling either `dispatch()` or
`audioMaster()`, the caller will oftentimes either pass along some kind of Actually sending and receiving the events happens in the
data structure through the void pointer function argument, or they expect the `EventHandler::send()` and `EventHandler::receive()` functions. When calling
function's return value to be a pointer to some kind of struct provided by either `dispatch()` or `audioMaster()`, the caller will oftentimes either
the plugin or host. The behaviour for reading from and writing into these pass along some kind of data structure through the void pointer function
void pointers and returning pointers to objects when needed is encapsulated argument, or they expect the function's return value to be a pointer to some
in the `DispatchDataConverter` and `HostCallbackDataCovnerter` classes for kind of struct provided by the plugin or host. The behaviour for reading from
the `dispatcher()` and `audioMaster()` functions respectively. For operations and writing into these void pointers and returning pointers to objects when
involving the plugin editor there is also some extra glue in needed is encapsulated in the `DispatchDataConverter` and
`Vst2Bridge::dispatch_wrapper`. On the receiving end of the function calls, `HostCallbackDataCovnerter` classes for the `dispatcher()` and
the `passthrough_event()` function which calls the callback functions and `audioMaster()` functions respectively. For operations involving the plugin
handles the marshalling between our data types created by the editor there is also some extra glue in `Vst2Bridge::dispatch_wrapper`. On
`*DataConverter` classes and the VST API's different pointer types. This the receiving end of the function calls, the `passthrough_event()` function
behaviour is separated from `receive_event()` so we can handle MIDI events which calls the callback functions and handles the marshalling between our
separately. This is needed because a select few plugins only store pointers data types created by the `*DataConverter` classes and the VST API's
to the received events rather than copies of the objects. Because of this, different pointer types. This behaviour is separated from `receive_event()`
the received event data must live at least until the next audio buffer gets so we can handle MIDI events separately. This is needed because a select few
processed so it needs to be stored temporarily. plugins only store pointers to the received events rather than copies of the
objects. Because of this, the received event data must live at least until
the next audio buffer gets processed so it needs to be stored temporarily.
6. The Wine VST host loads the Windows VST plugin and starts forwarding messages 6. The Wine VST host loads the Windows VST plugin and starts forwarding messages
over the sockets described above. over the sockets described above.
+88 -73
View File
@@ -28,79 +28,6 @@ namespace fs = boost::filesystem;
constexpr char alphanumeric_characters[] = constexpr char alphanumeric_characters[] =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
Sockets::Sockets(boost::asio::io_context& io_context,
const boost::filesystem::path& endpoint_base_dir,
bool listen)
: base_dir(endpoint_base_dir),
io_context(io_context),
host_vst_dispatch(io_context),
host_vst_dispatch_midi_events(io_context),
vst_host_callback(io_context),
host_vst_parameters(io_context),
host_vst_process_replacing(io_context),
host_vst_control(io_context),
host_vst_dispatch_endpoint(
(base_dir / "host_vst_dispatch.sock").string()),
host_vst_dispatch_midi_events_endpoint(
(base_dir / "host_vst_dispatch_midi_events.sock").string()),
vst_host_callback_endpoint(
(base_dir / "vst_host_callback.sock").string()),
host_vst_parameters_endpoint(
(base_dir / "host_vst_parameters.sock").string()),
host_vst_process_replacing_endpoint(
(base_dir / "host_vst_process_replacing.sock").string()),
host_vst_control_endpoint((base_dir / "host_vst_control.sock").string()) {
if (listen) {
fs::create_directory(base_dir);
acceptors = Acceptors{
.host_vst_dispatch{io_context, host_vst_dispatch_endpoint},
.host_vst_dispatch_midi_events{
io_context, host_vst_dispatch_midi_events_endpoint},
.vst_host_callback{io_context, vst_host_callback_endpoint},
.host_vst_parameters{io_context, host_vst_parameters_endpoint},
.host_vst_process_replacing{io_context,
host_vst_process_replacing_endpoint},
.host_vst_control{io_context, host_vst_control_endpoint},
};
}
}
Sockets::~Sockets() {
// Only clean if we're the ones who have created these files, although it
// should not cause any harm to also do this on the Wine side
if (acceptors) {
try {
fs::remove_all(base_dir);
} catch (const fs::filesystem_error&) {
// There should not be any filesystem errors since only one side
// removes the files, but if we somehow can't delete the file then
// we can just silently ignore this
}
}
}
void Sockets::connect() {
if (acceptors) {
acceptors->host_vst_dispatch.accept(host_vst_dispatch);
acceptors->host_vst_dispatch_midi_events.accept(
host_vst_dispatch_midi_events);
acceptors->vst_host_callback.accept(vst_host_callback);
acceptors->host_vst_parameters.accept(host_vst_parameters);
acceptors->host_vst_process_replacing.accept(
host_vst_process_replacing);
acceptors->host_vst_control.accept(host_vst_control);
} else {
host_vst_dispatch.connect(host_vst_dispatch_endpoint);
host_vst_dispatch_midi_events.connect(
host_vst_dispatch_midi_events_endpoint);
vst_host_callback.connect(vst_host_callback_endpoint);
host_vst_parameters.connect(host_vst_parameters_endpoint);
host_vst_process_replacing.connect(host_vst_process_replacing_endpoint);
host_vst_control.connect(host_vst_control_endpoint);
}
}
EventPayload DefaultDataConverter::read(const int /*opcode*/, EventPayload DefaultDataConverter::read(const int /*opcode*/,
const int /*index*/, const int /*index*/,
const intptr_t /*value*/, const intptr_t /*value*/,
@@ -154,6 +81,94 @@ intptr_t DefaultDataConverter::return_value(const int /*opcode*/,
return original; return original;
} }
EventHandler::EventHandler(
boost::asio::io_context& io_context,
boost::asio::local::stream_protocol::endpoint endpoint,
bool listen)
: endpoint(endpoint), socket(io_context) {
if (listen) {
fs::create_directories(fs::path(endpoint.path()).parent_path());
acceptor.emplace(io_context, endpoint);
}
}
void EventHandler::connect() {
if (acceptor) {
acceptor->accept(socket);
} else {
socket.connect(endpoint);
}
}
void EventHandler::close() {
socket.shutdown(boost::asio::local::stream_protocol::socket::shutdown_both);
socket.close();
}
Sockets::Sockets(boost::asio::io_context& io_context,
const boost::filesystem::path& endpoint_base_dir,
bool listen)
: base_dir(endpoint_base_dir),
host_vst_dispatch(io_context,
(base_dir / "host_vst_dispatch.sock").string(),
listen),
host_vst_dispatch_midi_events(
io_context,
(base_dir / "host_vst_dispatch_midi_events.sock").string(),
listen),
vst_host_callback(io_context,
(base_dir / "vst_host_callback.sock").string(),
listen),
host_vst_parameters(io_context),
host_vst_process_replacing(io_context),
host_vst_control(io_context),
host_vst_parameters_endpoint(
(base_dir / "host_vst_parameters.sock").string()),
host_vst_process_replacing_endpoint(
(base_dir / "host_vst_process_replacing.sock").string()),
host_vst_control_endpoint((base_dir / "host_vst_control.sock").string()) {
if (listen) {
fs::create_directories(base_dir);
acceptors = Acceptors{
.host_vst_parameters{io_context, host_vst_parameters_endpoint},
.host_vst_process_replacing{io_context,
host_vst_process_replacing_endpoint},
.host_vst_control{io_context, host_vst_control_endpoint},
};
}
}
Sockets::~Sockets() {
// Only clean if we're the ones who have created these files, although it
// should not cause any harm to also do this on the Wine side
if (acceptors) {
try {
fs::remove_all(base_dir);
} catch (const fs::filesystem_error&) {
// There should not be any filesystem errors since only one side
// removes the files, but if we somehow can't delete the file then
// we can just silently ignore this
}
}
}
void Sockets::connect() {
host_vst_dispatch.connect();
host_vst_dispatch_midi_events.connect();
vst_host_callback.connect();
if (acceptors) {
acceptors->host_vst_parameters.accept(host_vst_parameters);
acceptors->host_vst_process_replacing.accept(
host_vst_process_replacing);
acceptors->host_vst_control.accept(host_vst_control);
} else {
host_vst_parameters.connect(host_vst_parameters_endpoint);
host_vst_process_replacing.connect(host_vst_process_replacing_endpoint);
host_vst_control.connect(host_vst_control_endpoint);
}
}
boost::filesystem::path generate_endpoint_base(const std::string& plugin_name) { boost::filesystem::path generate_endpoint_base(const std::string& plugin_name) {
fs::path temp_directory = get_temporary_directory(); fs::path temp_directory = get_temporary_directory();
+376 -307
View File
@@ -36,206 +36,6 @@ using OutputAdapter = bitsery::OutputBufferAdapter<B>;
template <typename B> template <typename B>
using InputAdapter = bitsery::InputBufferAdapter<B>; using InputAdapter = bitsery::InputBufferAdapter<B>;
/**
* Manages all the sockets used for communicating between the plugin and the
* Wine host. Every plugin will get its own directory (the socket endpoint base
* directory), and all socket endpoints are created within this directory. This
* is usually `/run/user/<uid>/yabridge-<plugin_name>-<random_id>/`.
*
* On the plugin side this class should be initialized with `listen` set to
* `true` before launching the Wine VST host. This will start listening on the
* sockets, and the call to `connect()` will then accept any incoming
* connections.
*/
class Sockets {
public:
/**
* Sets up the sockets using the specified base directory. The sockets won't
* be active until `connect()` gets called.
*
* @param io_context The IO context the sockets should be bound to. Relevant
* when doing asynchronous operations.
* @param endpoint_base_dir The base directory that will be used for the
* Unix domain sockets.
* @param listen If `true`, start listening on the sockets. Incoming
* connections will be accepted when `connect()` gets called. This should
* be set to `true` on the plugin side, and `false` on the Wine host side.
*
* @see Sockets::connect
*/
Sockets(boost::asio::io_context& io_context,
const boost::filesystem::path& endpoint_base_dir,
bool listen);
/**
* Cleans up the directory containing the socket endpoints when yabridge
* shuts down if it still exists.
*/
~Sockets();
/**
* Depending on the value of the `listen` argument passed to the
* constructor, either accept connections made to the sockets on the Linux
* side or connect to the sockets on the Wine side
*/
void connect();
/**
* The base directory for our socket endpoints. All `*_endpoint` variables
* below are files within this directory.
*/
const boost::filesystem::path base_dir;
/**
* The IO context that the sockets will be created on. This is only relevant
* for asynchronous operations.
*/
boost::asio::io_context& io_context;
// The naming convention for these sockets is `<from>_<to>_<event>`. For
// instance the socket named `host_vst_dispatch` forwards
// `AEffect.dispatch()` calls from the native VST host to the Windows VST
// plugin (through the Wine VST host).
/**
* The socket that forwards all `dispatcher()` calls from the VST host to
* the plugin.
*/
boost::asio::local::stream_protocol::socket host_vst_dispatch;
/**
* Used specifically for the `effProcessEvents` opcode. This is needed
* because the Win32 API is designed to block during certain GUI
* interactions such as resizing a window or opening a dropdown. Without
* this MIDI input would just stop working at times.
*/
boost::asio::local::stream_protocol::socket host_vst_dispatch_midi_events;
/**
* The socket that forwards all `audioMaster()` calls from the Windows VST
* plugin to the host.
*/
boost::asio::local::stream_protocol::socket vst_host_callback;
/**
* Used for both `getParameter` and `setParameter` since they mostly
* overlap.
*/
boost::asio::local::stream_protocol::socket host_vst_parameters;
/**
* Used for processing audio usign the `process()`, `processReplacing()` and
* `processDoubleReplacing()` functions.
*/
boost::asio::local::stream_protocol::socket host_vst_process_replacing;
/**
* A control socket that sends data that is not suitable for the other
* sockets. At the moment this is only used to, on startup, send the Windows
* VST plugin's `AEffect` object to the native VST plugin, and to then send
* the configuration (from `config`) back to the Wine host.
*/
boost::asio::local::stream_protocol::socket host_vst_control;
private:
const boost::asio::local::stream_protocol::endpoint
host_vst_dispatch_endpoint;
const boost::asio::local::stream_protocol::endpoint
host_vst_dispatch_midi_events_endpoint;
const boost::asio::local::stream_protocol::endpoint
vst_host_callback_endpoint;
const boost::asio::local::stream_protocol::endpoint
host_vst_parameters_endpoint;
const boost::asio::local::stream_protocol::endpoint
host_vst_process_replacing_endpoint;
const boost::asio::local::stream_protocol::endpoint
host_vst_control_endpoint;
/**
* All of our socket acceptors. We have to create these before launching the
* Wine process.
*/
struct Acceptors {
boost::asio::local::stream_protocol::acceptor host_vst_dispatch;
boost::asio::local::stream_protocol::acceptor
host_vst_dispatch_midi_events;
boost::asio::local::stream_protocol::acceptor vst_host_callback;
boost::asio::local::stream_protocol::acceptor host_vst_parameters;
boost::asio::local::stream_protocol::acceptor
host_vst_process_replacing;
boost::asio::local::stream_protocol::acceptor host_vst_control;
};
/**
* If the `listen` constructor argument was set to `true`, when we'll
* prepare a set of socket acceptors that listen on the socket endpoints.
*/
std::optional<Acceptors> acceptors;
};
/**
* Encodes the base behavior for reading from and writing to the `data` argument
* for event dispatch functions. This provides base functionality for these
* kinds of events. The `dispatch()` function will require some more specific
* structs.
*/
class DefaultDataConverter {
public:
virtual ~DefaultDataConverter(){};
/**
* Read data from the `data` void pointer into a an `EventPayload` value
* that can be serialized and conveys the meaning of the event.
*/
virtual EventPayload read(const int opcode,
const int index,
const intptr_t value,
const void* data) const;
/**
* Read data from the `value` pointer into a an `EventPayload` value that
* can be serialized and conveys the meaning of the event. This is only used
* for the `effSetSpeakerArrangement` and `effGetSpeakerArrangement` events.
*/
virtual std::optional<EventPayload> read_value(const int opcode,
const intptr_t value) const;
/**
* Write the reponse back to the `data` pointer.
*/
virtual void write(const int opcode,
void* data,
const EventResult& response) const;
/**
* Write the reponse back to the `value` pointer. This is only used during
* the `effGetSpeakerArrangement` event.
*/
virtual void write_value(const int opcode,
intptr_t value,
const EventResult& response) const;
/**
* This function can override a callback's return value based on the opcode.
* This is used in one place to return a pointer to a `VstTime` object
* that's contantly being updated.
*
* @param opcode The opcode for the current event.
* @param original The original return value as returned by the callback
* function.
*/
virtual intptr_t return_value(const int opcode,
const intptr_t original) const;
};
/**
* Generate a unique base directory that can be used as a prefix for all Unix
* domain socket endpoints used in `PluginBridge`/`Vst2Bridge`. This will
* usually return `/run/user/<uid>/yabridge-<plugin_name>-<random_id>/`.
*
* Sockets for group hosts are handled separately. See
* `../plugin/utils.h:generate_group_endpoint` for more information on those.
*
* @param plugin_name The name of the plugin we're generating endpoints for.
* Used as a visual indication of what plugin is using this endpoint.
*/
boost::filesystem::path generate_endpoint_base(const std::string& plugin_name);
/** /**
* Serialize an object using bitsery and write it to a socket. This will write * Serialize an object using bitsery and write it to a socket. This will write
* both the size of the serialized object and the object itself over the socket. * both the size of the serialized object and the object itself over the socket.
@@ -320,126 +120,395 @@ inline T read_object(Socket& socket,
} }
/** /**
* Serialize and send an event over a socket. This is used for both the host -> * Encodes the base behavior for reading from and writing to the `data` argument
* plugin 'dispatch' events and the plugin -> host 'audioMaster' host callbacks * for event dispatch functions. This provides base functionality for these
* since they follow the same format. See one of those functions for details on * kinds of events. The `dispatch()` function will require some more specific
* the parameters and return value of this function. * structs.
*
* @param socket The socket to write over, should be the same socket the other
* endpoint is using to call `receive_event()`.
* @param write_mutex A mutex to ensure that only one thread can write to
* the socket at once. Needed because VST hosts and plugins can and sometimes
* will call the `dispatch()` or `audioMaster()` functions from multiple
* threads at once.
* @param data_converter Some struct that knows how to read data from and write
* data back to the `data` void pointer. For host callbacks this parameter
* contains either a string or a null pointer while `dispatch()` calls might
* contain opcode specific structs. See the documentation for `EventPayload`
* for more information. The `DefaultDataConverter` defined above handles the
* basic behavior that's sufficient for host callbacks.
* @param logging A pair containing a logger instance and whether or not this is
* for sending `dispatch()` events or host callbacks. Optional since it
* doesn't have to be done on both sides.
*
* @relates receive_event
* @relates passthrough_event
*/ */
template <typename D> class DefaultDataConverter {
intptr_t send_event(boost::asio::local::stream_protocol::socket& socket, public:
std::mutex& write_mutex, virtual ~DefaultDataConverter(){};
D& data_converter,
std::optional<std::pair<Logger&, bool>> logging,
int opcode,
int index,
intptr_t value,
void* data,
float option) {
// Encode the right payload types for this event. Check the documentation
// for `EventPayload` for more information. These types are converted to
// C-style data structures in `passthrough_event()` so they can be passed to
// a plugin or callback function.
const EventPayload payload =
data_converter.read(opcode, index, value, data);
const std::optional<EventPayload> value_payload =
data_converter.read_value(opcode, value);
if (logging) { /**
auto [logger, is_dispatch] = *logging; * Read data from the `data` void pointer into a an `EventPayload` value
logger.log_event(is_dispatch, opcode, index, value, payload, option, * that can be serialized and conveys the meaning of the event.
value_payload); */
} virtual EventPayload read(const int opcode,
const int index,
const intptr_t value,
const void* data) const;
const Event event{.opcode = opcode, /**
.index = index, * Read data from the `value` pointer into a an `EventPayload` value that
.value = value, * can be serialized and conveys the meaning of the event. This is only used
.option = option, * for the `effSetSpeakerArrangement` and `effGetSpeakerArrangement` events.
.payload = payload, */
.value_payload = value_payload}; virtual std::optional<EventPayload> read_value(const int opcode,
const intptr_t value) const;
// Prevent two threads from writing over the socket at the same time and /**
// messages getting out of order. This is needed because we can't prevent * Write the reponse back to the `data` pointer.
// the plugin or the host from calling `dispatch()` or `audioMaster()` from */
// multiple threads. virtual void write(const int opcode,
EventResult response; void* data,
{ const EventResult& response) const;
std::lock_guard lock(write_mutex);
write_object(socket, event);
response = read_object<EventResult>(socket);
}
if (logging) { /**
auto [logger, is_dispatch] = *logging; * Write the reponse back to the `value` pointer. This is only used during
logger.log_event_response(is_dispatch, opcode, response.return_value, * the `effGetSpeakerArrangement` event.
response.payload, response.value_payload); */
} virtual void write_value(const int opcode,
intptr_t value,
const EventResult& response) const;
data_converter.write(opcode, data, response); /**
data_converter.write_value(opcode, value, response); * This function can override a callback's return value based on the opcode.
* This is used in one place to return a pointer to a `VstTime` object
return data_converter.return_value(opcode, response.return_value); * that's contantly being updated.
} *
* @param opcode The opcode for the current event.
* @param original The original return value as returned by the callback
* function.
*/
virtual intptr_t return_value(const int opcode,
const intptr_t original) const;
};
/** /**
* Receive an event from a socket, call a function to generate a response, and * For most of our sockets we can just send out our messages on the writing
* write the response back over the socket. This is usually used together with * side, and do a simple blocking loop on the reading side. The `dispatch()` and
* `passthrough_event()` which passes the event data through to an event * `audioMaster()` calls are different. Not only do they have they come with
* dispatcher function. This behaviour is split into two functions to avoid * complex payload values, they can also be called simultaneously from multiple
* redundant data conversions when handling MIDI data, as some plugins require * threads, and `audioMaster()` and `dispatch()` calls can even be mutually
* the received data to be temporarily stored until the next event audio buffer * recursive. Luckily this does not happen very often, but it does mean that our
* gets processed. * simple 'one-socket-per-function' model doesn't work anymore. Because setting
* up new sockets is quite expensive and this is seldom needed, this works
* slightly differently:
* *
* @param socket The socket to receive on and to send the response back to. * - We'll keep a single long lived socket connection. This works the exact same
* @param logging A pair containing a logger instance and whether or not this is * way as every other socket defined in the `Sockets` class.
* for sending `dispatch()` events or host callbacks. Optional since it * - Aside from that the listening side will have a second thread asynchronously
* doesn't have to be done on both sides. * listening for new connections on the socket endpoint.
* @param callback The function used to generate a response out of an event.
* *
* @tparam F A function type in the form of `EventResponse(Event)`. * The `EventHandler::send()` is used to send events. If the socket is currently
* * being written to, we'll first create a new socket connection as described
* @relates send_event * above. Similarly, the `EventHandler::receive()` method first sets up
* @relates passthrough_event * asynchronous listeners for the socket endpoint, and then block and handle
* events until the main socket is closed.
*/ */
template <typename F> class EventHandler {
void receive_event(boost::asio::local::stream_protocol::socket& socket, public:
std::optional<std::pair<Logger&, bool>> logging, /**
F callback) { * Sets up a single main socket for this type of events. The sockets won't
auto event = read_object<Event>(socket); * be active until `connect()` gets called.
if (logging) { *
auto [logger, is_dispatch] = *logging; * @param io_context The IO context the sockets should be bound to. Relevant
logger.log_event(is_dispatch, event.opcode, event.index, event.value, * when doing asynchronous operations.
event.payload, event.option, event.value_payload); * @param endpoint The socket endpoint used for this event handler.
* @param listen If `true`, start listening on the sockets. Incoming
* connections will be accepted when `connect()` gets called. This should
* be set to `true` on the plugin side, and `false` on the Wine host side.
*
* @see Sockets::connect
*/
EventHandler(boost::asio::io_context& io_context,
boost::asio::local::stream_protocol::endpoint endpoint,
bool listen);
/**
* Depending on the value of the `listen` argument passed to the
* constructor, either accept connections made to the sockets on the Linux
* side or connect to the sockets on the Wine side
*/
void connect();
/**
* Close the socket. Both sides that are actively listening will be thrown a
* `boost::system_error` when this happens.
*/
void close();
/**
* Serialize and send an event over a socket. This is used for both the host
* -> plugin 'dispatch' events and the plugin -> host 'audioMaster' host
* callbacks since they follow the same format. See one of those functions
* for details on the parameters and return value of this function.
*
* As described above, if this function is currently being called from
* another thread, then this will create a new socket connection and send
* the event there instead.
*
* @param data_converter Some struct that knows how to read data from and
* write data back to the `data` void pointer. For host callbacks this
* parameter contains either a string or a null pointer while `dispatch()`
* calls might contain opcode specific structs. See the documentation for
* `EventPayload` for more information. The `DefaultDataConverter` defined
* above handles the basic behavior that's sufficient for host callbacks.
* @param logging A pair containing a logger instance and whether or not
* this is for sending `dispatch()` events or host callbacks. Optional
* since it doesn't have to be done on both sides.
*
* @relates EventHandler::receive
* @relates passthrough_event
*/
template <typename D>
intptr_t send(D& data_converter,
std::optional<std::pair<Logger&, bool>> logging,
int opcode,
int index,
intptr_t value,
void* data,
float option) {
// TODO: Create a new socket if the mutex is locked
// Encode the right payload types for this event. Check the
// documentation for `EventPayload` for more information. These types
// are converted to C-style data structures in `passthrough_event()` so
// they can be passed to a plugin or callback function.
const EventPayload payload =
data_converter.read(opcode, index, value, data);
const std::optional<EventPayload> value_payload =
data_converter.read_value(opcode, value);
if (logging) {
auto [logger, is_dispatch] = *logging;
logger.log_event(is_dispatch, opcode, index, value, payload, option,
value_payload);
}
const Event event{.opcode = opcode,
.index = index,
.value = value,
.option = option,
.payload = payload,
.value_payload = value_payload};
// Prevent two threads from writing over the socket at the same time and
// messages getting out of order. This is needed because we can't
// prevent the plugin or the host from calling `dispatch()` or
// `audioMaster()` from multiple threads.
EventResult response;
{
std::lock_guard lock(write_mutex);
write_object(socket, event);
response = read_object<EventResult>(socket);
}
if (logging) {
auto [logger, is_dispatch] = *logging;
logger.log_event_response(is_dispatch, opcode,
response.return_value, response.payload,
response.value_payload);
}
data_converter.write(opcode, data, response);
data_converter.write_value(opcode, value, response);
return data_converter.return_value(opcode, response.return_value);
} }
EventResult response = callback(event); /**
if (logging) { * Spawn a new thread to listen for extra connections to `endpoint`, and
auto [logger, is_dispatch] = *logging; * then a blocking loop that handles events from the primary `socket`.
logger.log_event_response(is_dispatch, event.opcode, *
response.return_value, response.payload, * The specified function will be used to create an `EventResult` from an
response.value_payload); * `Event`. This is almost always a wrapper around `passthrough_event()`,
* which converts the `EventPayload` into a format used by VST2, calls
* either `dispatch()` or `audioMaster()` depending on the socket, and then
* serializes the result back into an `EventResultPayload`.
*
* This function will also be used separately for receiving MIDI data, as
* some plugins will need pointers to received MIDI data to stay alive until
* the next audio buffer gets processed.
*
* @param logging A pair containing a logger instance and whether or not
* this is for sending `dispatch()` events or host callbacks. Optional since
* it doesn't have to be done on both sides.
* @param callback The function used to generate a response out of an event.
*
* @tparam F A function type in the form of `EventResponse(Event)`.
*
* @relates EventHandler::send
* @relates passthrough_event
*/
template <typename F>
void receive(std::optional<std::pair<Logger&, bool>> logging, F callback) {
// TODO: Listen for new incoming connections
while (true) {
try {
auto event = read_object<Event>(socket);
if (logging) {
auto [logger, is_dispatch] = *logging;
logger.log_event(is_dispatch, event.opcode, event.index,
event.value, event.payload, event.option,
event.value_payload);
}
EventResult response = callback(event);
if (logging) {
auto [logger, is_dispatch] = *logging;
logger.log_event_response(
is_dispatch, event.opcode, response.return_value,
response.payload, response.value_payload);
}
write_object(socket, response);
} catch (const boost::system::system_error&) {
// This happens when the sockets got closed because the plugin
// is being shut down
break;
}
}
} }
write_object(socket, response); private:
} boost::asio::local::stream_protocol::endpoint endpoint;
boost::asio::local::stream_protocol::socket socket;
/**
* This acceptor will be used once synchronously on the listening side
* during `Sockets::connect()`. When `EventHandler::receive()` is called
* this will be replaced by a new acceptor bound to a new IO context to
* receive any additional incoming connections.
*/
std::optional<boost::asio::local::stream_protocol::acceptor> acceptor;
/**
* A mutex that locks the main `socket`. If this is locked, then any new
* events will be sent over a new socket instead.
*/
std::mutex write_mutex;
};
/**
* Manages all the sockets used for communicating between the plugin and the
* Wine host. Every plugin will get its own directory (the socket endpoint base
* directory), and all socket endpoints are created within this directory. This
* is usually `/run/user/<uid>/yabridge-<plugin_name>-<random_id>/`.
*
* On the plugin side this class should be initialized with `listen` set to
* `true` before launching the Wine VST host. This will start listening on the
* sockets, and the call to `connect()` will then accept any incoming
* connections.
*/
class Sockets {
public:
/**
* Sets up the sockets using the specified base directory. The sockets won't
* be active until `connect()` gets called.
*
* @param io_context The IO context the sockets should be bound to. Relevant
* when doing asynchronous operations.
* @param endpoint_base_dir The base directory that will be used for the
* Unix domain sockets.
* @param listen If `true`, start listening on the sockets. Incoming
* connections will be accepted when `connect()` gets called. This should
* be set to `true` on the plugin side, and `false` on the Wine host side.
*
* @see Sockets::connect
*/
Sockets(boost::asio::io_context& io_context,
const boost::filesystem::path& endpoint_base_dir,
bool listen);
/**
* Cleans up the directory containing the socket endpoints when yabridge
* shuts down if it still exists.
*/
~Sockets();
/**
* Depending on the value of the `listen` argument passed to the
* constructor, either accept connections made to the sockets on the Linux
* side or connect to the sockets on the Wine side
*/
void connect();
/**
* The base directory for our socket endpoints. All `*_endpoint` variables
* below are files within this directory.
*/
const boost::filesystem::path base_dir;
// The naming convention for these sockets is `<from>_<to>_<event>`. For
// instance the socket named `host_vst_dispatch` forwards
// `AEffect.dispatch()` calls from the native VST host to the Windows VST
// plugin (through the Wine VST host).
/**
* The socket that forwards all `dispatcher()` calls from the VST host to
* the plugin.
*/
EventHandler host_vst_dispatch;
/**
* Used specifically for the `effProcessEvents` opcode. This is needed
* because the Win32 API is designed to block during certain GUI
* interactions such as resizing a window or opening a dropdown. Without
* this MIDI input would just stop working at times.
*/
EventHandler host_vst_dispatch_midi_events;
/**
* The socket that forwards all `audioMaster()` calls from the Windows VST
* plugin to the host.
*/
EventHandler vst_host_callback;
/**
* Used for both `getParameter` and `setParameter` since they mostly
* overlap.
*/
boost::asio::local::stream_protocol::socket host_vst_parameters;
/**
* Used for processing audio usign the `process()`, `processReplacing()` and
* `processDoubleReplacing()` functions.
*/
boost::asio::local::stream_protocol::socket host_vst_process_replacing;
/**
* A control socket that sends data that is not suitable for the other
* sockets. At the moment this is only used to, on startup, send the Windows
* VST plugin's `AEffect` object to the native VST plugin, and to then send
* the configuration (from `config`) back to the Wine host.
*/
boost::asio::local::stream_protocol::socket host_vst_control;
private:
const boost::asio::local::stream_protocol::endpoint
host_vst_parameters_endpoint;
const boost::asio::local::stream_protocol::endpoint
host_vst_process_replacing_endpoint;
const boost::asio::local::stream_protocol::endpoint
host_vst_control_endpoint;
/**
* All of our socket acceptors. We have to create these before launching the
* Wine process.
*/
struct Acceptors {
boost::asio::local::stream_protocol::acceptor host_vst_parameters;
boost::asio::local::stream_protocol::acceptor
host_vst_process_replacing;
boost::asio::local::stream_protocol::acceptor host_vst_control;
};
/**
* If the `listen` constructor argument was set to `true`, when we'll
* prepare a set of socket acceptors that listen on the socket endpoints.
*/
std::optional<Acceptors> acceptors;
};
/**
* Generate a unique base directory that can be used as a prefix for all Unix
* domain socket endpoints used in `PluginBridge`/`Vst2Bridge`. This will
* usually return `/run/user/<uid>/yabridge-<plugin_name>-<random_id>/`.
*
* Sockets for group hosts are handled separately. See
* `../plugin/utils.h:generate_group_endpoint` for more information on those.
*
* @param plugin_name The name of the plugin we're generating endpoints for.
* Used as a visual indication of what plugin is using this endpoint.
*/
boost::filesystem::path generate_endpoint_base(const std::string& plugin_name);
/** /**
* Create a callback function that takes an `Event` object, decodes the data * Create a callback function that takes an `Event` object, decodes the data
@@ -460,9 +529,9 @@ void receive_event(boost::asio::local::stream_protocol::socket& socket,
* `audioMasterCallback`. * `audioMasterCallback`.
* *
* @return A `EventResult(Event)` callback function that can be passed to * @return A `EventResult(Event)` callback function that can be passed to
* `receive_event`. * `EditorHandler::receive()`.
* *
* @relates receive_event * @relates EditorHandler::receive
*/ */
template <typename F> template <typename F>
auto passthrough_event(AEffect* plugin, F callback) { auto passthrough_event(AEffect* plugin, F callback) {
-2
View File
@@ -263,7 +263,5 @@ void GroupHost::terminate() {
// There's no need to manually terminate group host processes as they will // There's no need to manually terminate group host processes as they will
// shut down automatically after all plugins have exited. Manually closing // shut down automatically after all plugins have exited. Manually closing
// the dispatch socket will cause the associated plugin to exit. // the dispatch socket will cause the associated plugin to exit.
sockets.host_vst_dispatch.shutdown(
boost::asio::local::stream_protocol::socket::shutdown_both);
sockets.host_vst_dispatch.close(); sockets.host_vst_dispatch.close();
} }
+32 -44
View File
@@ -121,41 +121,31 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
// instead of asynchronous IO since communication has to be handled in // instead of asynchronous IO since communication has to be handled in
// lockstep anyway // lockstep anyway
host_callback_handler = std::jthread([&]() { host_callback_handler = std::jthread([&]() {
while (true) { // TODO: Think of a nicer way to structure this and the similar
try { // handler in `Vst2Bridge::handle_dispatch_midi_events`
// TODO: Think of a nicer way to structure this and the similar sockets.vst_host_callback.receive(
// handler in `Vst2Bridge::handle_dispatch_midi_events` std::pair<Logger&, bool>(logger, false), [&](Event& event) {
receive_event( // MIDI events sent from the plugin back to the host are a
sockets.vst_host_callback, // special case here. They have to sent during the
std::pair<Logger&, bool>(logger, false), [&](Event& event) { // `processReplacing()` function or else the host will ignore
// MIDI events sent from the plugin back to the host are // them. Because of this we'll temporarily save any MIDI events
// a special case here. They have to sent during the // we receive here, and then we'll actually send them to the
// `processReplacing()` function or else the host will // host at the end of the `process_replacing()` function.
// ignore them. Because of this we'll temporarily save if (event.opcode == audioMasterProcessEvents) {
// any MIDI events we receive here, and then we'll std::lock_guard lock(incoming_midi_events_mutex);
// actually send them to the host at the end of the
// `process_replacing()` function.
if (event.opcode == audioMasterProcessEvents) {
std::lock_guard lock(incoming_midi_events_mutex);
incoming_midi_events.push_back( incoming_midi_events.push_back(
std::get<DynamicVstEvents>(event.payload)); std::get<DynamicVstEvents>(event.payload));
EventResult response{.return_value = 1, EventResult response{.return_value = 1,
.payload = nullptr, .payload = nullptr,
.value_payload = std::nullopt}; .value_payload = std::nullopt};
return response; return response;
} else { } else {
return passthrough_event( return passthrough_event(&plugin,
&plugin, host_callback_function)(event); host_callback_function)(event);
} }
}); });
} catch (const boost::system::system_error&) {
// This happens when the sockets got closed because the plugin
// is being shut down
break;
}
}
}); });
// Read the plugin's information from the Wine process. This can only be // Read the plugin's information from the Wine process. This can only be
@@ -435,10 +425,9 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
intptr_t return_value = 0; intptr_t return_value = 0;
try { try {
// TODO: Add some kind of timeout? // TODO: Add some kind of timeout?
return_value = send_event( return_value = sockets.host_vst_dispatch.send(
sockets.host_vst_dispatch, dispatch_mutex, converter, converter, std::pair<Logger&, bool>(logger, true), opcode,
std::pair<Logger&, bool>(logger, true), opcode, index, index, value, data, option);
value, data, option);
} catch (const boost::system::system_error& a) { } catch (const boost::system::system_error& a) {
// Thrown when the socket gets closed because the VST plugin // Thrown when the socket gets closed because the VST plugin
// loaded into the Wine process crashed during shutdown // loaded into the Wine process crashed during shutdown
@@ -461,10 +450,9 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
// thread and socket to pass MIDI events. Otherwise plugins will // thread and socket to pass MIDI events. Otherwise plugins will
// stop receiving MIDI data when they have an open dropdowns or // stop receiving MIDI data when they have an open dropdowns or
// message box. // message box.
return send_event(sockets.host_vst_dispatch_midi_events, return sockets.host_vst_dispatch_midi_events.send(
dispatch_midi_events_mutex, converter, converter, std::pair<Logger&, bool>(logger, true), opcode,
std::pair<Logger&, bool>(logger, true), opcode, index, value, data, option);
index, value, data, option);
break; break;
case effCanDo: { case effCanDo: {
const std::string query(static_cast<const char*>(data)); const std::string query(static_cast<const char*>(data));
@@ -521,9 +509,9 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
// and loading plugin state it's much better to have bitsery or our // and loading plugin state it's much better to have bitsery or our
// receiving function temporarily allocate a large enough buffer rather than // receiving function temporarily allocate a large enough buffer rather than
// to have a bunch of allocated memory sitting around doing nothing. // to have a bunch of allocated memory sitting around doing nothing.
return send_event(sockets.host_vst_dispatch, dispatch_mutex, converter, return sockets.host_vst_dispatch.send(
std::pair<Logger&, bool>(logger, true), opcode, index, converter, std::pair<Logger&, bool>(logger, true), opcode, index, value,
value, data, option); data, option);
} }
template <typename T> template <typename T>
+5 -11
View File
@@ -23,9 +23,9 @@
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include "../common/communication.h"
#include "../common/configuration.h" #include "../common/configuration.h"
#include "../common/logging.h" #include "../common/logging.h"
#include "../common/communication.h"
#include "host-process.h" #include "host-process.h"
/** /**
@@ -132,16 +132,10 @@ class PluginBridge {
std::jthread host_callback_handler; std::jthread host_callback_handler;
/** /**
* A binary semaphore to prevent race conditions from the dispatch function * A mutex to prevent multiple simultaneous calls to `getParameter()` and
* being called by two threads at once. See `send_event()` for more * `setParameter()`. This likely won't happen, but better safe than sorry.
* information. * For `dispatch()` and `audioMaster()` there's some more complex logic for
*/ * this in `EventHandler`.
std::mutex dispatch_mutex;
std::mutex dispatch_midi_events_mutex;
/**
* A similar semaphore as the `dispatch_*` semaphores in the rare case that
* `getParameter()` and `setParameter()` are being called at the same time
* since they use the same socket.
*/ */
std::mutex parameters_mutex; std::mutex parameters_mutex;
+58 -80
View File
@@ -144,96 +144,74 @@ bool Vst2Bridge::should_skip_message_loop() const {
} }
void Vst2Bridge::handle_dispatch() { void Vst2Bridge::handle_dispatch() {
while (true) { sockets.host_vst_dispatch.receive(
try { std::nullopt,
receive_event( passthrough_event(
sockets.host_vst_dispatch, std::nullopt, plugin,
passthrough_event( [&](AEffect* plugin, int opcode, int index, intptr_t value,
plugin, void* data, float option) -> intptr_t {
[&](AEffect* plugin, int opcode, int index, intptr_t value, // Instead of running `plugin->dispatcher()` (or
void* data, float option) -> intptr_t { // `dispatch_wrapper()`) directly, we'll run the function within
// Instead of running `plugin->dispatcher()` (or // the IO context so all events will be executed on the same
// `dispatch_wrapper()`) directly, we'll run the // thread as the one that runs the Win32 message loop
// function within the IO context so all events will be std::promise<intptr_t> dispatch_result;
// executed on the same thread as the one that runs the boost::asio::dispatch(io_context, [&]() {
// Win32 message loop const intptr_t result = dispatch_wrapper(
std::promise<intptr_t> dispatch_result; plugin, opcode, index, value, data, option);
boost::asio::dispatch(io_context, [&]() {
const intptr_t result = dispatch_wrapper(
plugin, opcode, index, value, data, option);
dispatch_result.set_value(result); dispatch_result.set_value(result);
}); });
// The message loop and X11 event handling will be run // The message loop and X11 event handling will be run
// separately on a timer // separately on a timer
return dispatch_result.get_future().get(); return dispatch_result.get_future().get();
})); }));
} catch (const boost::system::system_error&) {
// The plugin has cut off communications, so we can shut down this
// host application
break;
}
}
} }
void Vst2Bridge::handle_dispatch_midi_events() { void Vst2Bridge::handle_dispatch_midi_events() {
while (true) { sockets.host_vst_dispatch_midi_events.receive(
try { std::nullopt, [&](Event& event) {
receive_event( if (BOOST_LIKELY(event.opcode == effProcessEvents)) {
sockets.host_vst_dispatch_midi_events, std::nullopt, // For 99% of the plugins we can just call
[&](Event& event) { // `effProcessReplacing()` and be done with it, but a select few
if (BOOST_LIKELY(event.opcode == effProcessEvents)) { // plugins (I could only find Kontakt that does this) don't
// For 99% of the plugins we can just call // actually make copies of the events they receive and only
// `effProcessReplacing()` and be done with it, but a // store pointers, meaning that they have to live at least until
// select few plugins (I could only find Kontakt that // the next audio buffer gets processed. We're not using
// does this) don't actually make copies of the events // `passthrough_events()` here directly because we need to store
// they receive and only store pointers, meaning that // a copy of the `DynamicVstEvents` struct before passing the
// they have to live at least until the next audio // generated `VstEvents` object to the plugin.
// buffer gets processed. We're not using std::lock_guard lock(next_buffer_midi_events_mutex);
// `passhtourhg_events()` here directly because we need
// to store a copy of the `DynamicVstEvents` struct
// before passing the generated `VstEvents` object to
// the plugin.
std::lock_guard lock(next_buffer_midi_events_mutex);
next_audio_buffer_midi_events.push_back( next_audio_buffer_midi_events.push_back(
std::get<DynamicVstEvents>(event.payload)); std::get<DynamicVstEvents>(event.payload));
DynamicVstEvents& events = DynamicVstEvents& events = next_audio_buffer_midi_events.back();
next_audio_buffer_midi_events.back();
// Exact same handling as in `passthrough_event`, apart // Exact same handling as in `passthrough_event`, apart from
// from making a copy of the events first // making a copy of the events first
const intptr_t return_value = plugin->dispatcher( const intptr_t return_value = plugin->dispatcher(
plugin, event.opcode, event.index, event.value, plugin, event.opcode, event.index, event.value,
&events.as_c_events(), event.option); &events.as_c_events(), event.option);
EventResult response{.return_value = return_value, EventResult response{.return_value = return_value,
.payload = nullptr, .payload = nullptr,
.value_payload = std::nullopt}; .value_payload = std::nullopt};
return response; return response;
} else { } else {
using namespace std::placeholders; using namespace std::placeholders;
std::cerr << "[Warning] Received non-MIDI " std::cerr << "[Warning] Received non-MIDI "
"event on MIDI processing thread" "event on MIDI processing thread"
<< std::endl; << std::endl;
// Maybe this should just be a hard error instead, since // Maybe this should just be a hard error instead, since it
// it should never happen // should never happen
return passthrough_event( return passthrough_event(
plugin, plugin, std::bind(&Vst2Bridge::dispatch_wrapper, this, _1,
std::bind(&Vst2Bridge::dispatch_wrapper, this, _1,
_2, _3, _4, _5, _6))(event); _2, _3, _4, _5, _6))(event);
} }
}); });
} catch (const boost::system::system_error&) {
// The plugin has cut off communications, so we can shut down this
// host application
break;
}
}
} }
void Vst2Bridge::handle_parameters() { void Vst2Bridge::handle_parameters() {
@@ -569,8 +547,8 @@ intptr_t Vst2Bridge::host_callback(AEffect* effect,
} }
HostCallbackDataConverter converter(effect, time_info); HostCallbackDataConverter converter(effect, time_info);
return send_event(sockets.vst_host_callback, host_callback_mutex, converter, return sockets.vst_host_callback.send(converter, std::nullopt, opcode,
std::nullopt, opcode, index, value, data, option); index, value, data, option);
} }
intptr_t VST_CALL_CONV host_callback_proxy(AEffect* effect, intptr_t VST_CALL_CONV host_callback_proxy(AEffect* effect,
+1 -8
View File
@@ -29,9 +29,9 @@
#include <boost/asio/local/stream_protocol.hpp> #include <boost/asio/local/stream_protocol.hpp>
#include <mutex> #include <mutex>
#include "../../common/communication.h"
#include "../../common/configuration.h" #include "../../common/configuration.h"
#include "../../common/logging.h" #include "../../common/logging.h"
#include "../../common/communication.h"
#include "../editor.h" #include "../editor.h"
#include "../utils.h" #include "../utils.h"
@@ -210,13 +210,6 @@ class Vst2Bridge {
*/ */
Win32Thread process_replacing_handler; Win32Thread process_replacing_handler;
/**
* A binary semaphore to prevent race conditions from the host callback
* function being called by two threads at once. See `send_event()` for more
* information.
*/
std::mutex host_callback_mutex;
/** /**
* A scratch buffer for sending and receiving data during `process` and * A scratch buffer for sending and receiving data during `process` and
* `processReplacing` calls. * `processReplacing` calls.