mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-07 03:50:11 +02:00
Merge branch 'feature/socket-rework' into master
This commit is contained in:
@@ -6,6 +6,45 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic
|
||||
Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- The way communication works in yabridge has been completely redesigned to be
|
||||
asynchronous and to use additional threads as necessary. This was needed to
|
||||
allow yabridge to handle nested and mutually recursive function calls as well
|
||||
as several other edge cases. What this boils down to is that yabridge became
|
||||
even faster, more responsive, and can now handle a few edge case scenarios
|
||||
that would previously require workarounds. This means that yabridge no longer
|
||||
requires the `hack_reaper_update_display` workaround for _REAPER_ and
|
||||
_Renoise_, that the loading issues in Bitwig Studio 3.3 beta 1 have also been
|
||||
resolved, and that certain plugins like Kontakt no longer interrupt playback
|
||||
in Bitwig while their editor is opening. I have been testing this extensively
|
||||
to make sure that the change does not introduce any regressions, but please
|
||||
let me know if this does break anything for you.
|
||||
|
||||
TODO: Expand on this
|
||||
TODO: Remove known issue about opening Kontakt and certain other plugins
|
||||
causing playback to stall, since this is no longer the case
|
||||
|
||||
### Changed
|
||||
|
||||
- As part of the communication rework the way the Wine process handles threading
|
||||
has also been completely reworked.
|
||||
|
||||
### Removed
|
||||
|
||||
- The `hack_reaper_update_display` option is now obsolete and has been removed.
|
||||
|
||||
TODO: Remove all mentions of `hack_reaper_update_display` from the readme.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a very long standing issue when using plugins groups where unloading a
|
||||
plugin could cause a crash. In practice this was only reproducible during the
|
||||
plugin scanning process when hosting a very large number of plugins in a
|
||||
single group.
|
||||
|
||||
## [1.7.1] - 2020-10-23
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -38,10 +38,10 @@ compatibility while also staying easy to debug and maintain.
|
||||
Yabridge has been tested under the following VST hosts using Wine Staging 5.9:
|
||||
|
||||
- Bitwig Studio 3.2
|
||||
- Carla 2.1
|
||||
- Ardour 6.2
|
||||
- Carla 2.2
|
||||
- Ardour 6.3
|
||||
- Mixbus 6.0.702
|
||||
- REAPER 6.09[\*](#runtime-dependencies-and-known-issues)
|
||||
- REAPER 6.15[\*](#runtime-dependencies-and-known-issues)
|
||||
- Renoise 3.2.1[\*](#runtime-dependencies-and-known-issues)
|
||||
|
||||
Please let me know if there are any issues with other VST hosts.
|
||||
@@ -499,6 +499,7 @@ the following dependencies:
|
||||
The following dependencies are included in the repository as a Meson wrap:
|
||||
|
||||
- bitsery
|
||||
- function2
|
||||
- tomlplusplus
|
||||
|
||||
The project can then be compiled as follows:
|
||||
|
||||
+40
-34
@@ -1,5 +1,7 @@
|
||||
# Architecture
|
||||
|
||||
<!-- TODO: Mention the new special socket approach for `dispatch()` and `audioMaster()-->
|
||||
|
||||
The project consists of two components: a Linux native VST plugin
|
||||
(`libyabridge.so`) and a VST host that runs under Wine
|
||||
(`yabridge-host.exe`/`yabridge-host.exe.so`, and
|
||||
@@ -38,22 +40,24 @@ as the _Windows VST plugin_. The whole process works as follows:
|
||||
- The Wine prefix the plugin is located in. If the `WINEPREFIX` environment
|
||||
variable is specified, then that will be used instead.
|
||||
|
||||
3. The plugin then sets up a Unix domain socket endpoint to communicate with the
|
||||
Wine VST host somewhere in a temporary directory and starts listening on it.
|
||||
I chose to communicate over Unix domain sockets rather than using shared
|
||||
memory directly because this way you get low latency communication with
|
||||
without any busy waits or manual synchronisation for free. The added benefit
|
||||
is that it also makes it possible to send arbitrarily large chunks of data
|
||||
without having to split it up first. This is useful for transmitting audio
|
||||
and preset data which may have any arbitrary size.
|
||||
3. The plugin then sets up several Unix domain socket endpoints to communicate
|
||||
with the Wine VST host somewhere in a temporary directory and starts
|
||||
listening on them. We'll use multiple sockets so we can easily handle
|
||||
multiple data streams from different threads using blocking synchronous
|
||||
operations. This greatly simplifies the way communication works without
|
||||
compromising on latency. The different sockets will be described below. We
|
||||
communicate over Unix domain sockets rather than using shared memory directly
|
||||
because this way we get low latency communication without any manual
|
||||
synchronisation for free, while being able to send messages of arbitrary
|
||||
length without having to split them up first. This is useful for transmitting
|
||||
audio and preset data which can be any arbitrary size.
|
||||
4. The plugin launches the Wine VST host in the detected wine prefix, passing
|
||||
the name of the `.dll` file it should be loading and the path to the Unix
|
||||
domain socket that was just created as its arguments.
|
||||
5. Communication gets set up using multiple sockets over the end point created
|
||||
previously. This allows us to easily handle multiple data streams from
|
||||
different threads using blocking read operations for synchronization. Doing
|
||||
this greatly simplifies the way communication works without compromising on
|
||||
latency. The following types of events each get their own socket:
|
||||
the name of the `.dll` file it should be loading and the base directory for
|
||||
the Unix domain sockets that are going to be communciated over as its
|
||||
arguments.
|
||||
5. The Wine VST host connects to the sockets and communication between the
|
||||
plugin and the Wine VST host gets set up. The following types of events each
|
||||
get their own socket:
|
||||
|
||||
- Calls from the native VST host to the plugin's `dispatcher()` function.
|
||||
These get forwarded to the Windows VST plugin through the Wine VST host.
|
||||
@@ -103,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
|
||||
is handled using [bitsery](https://github.com/fraillt/bitsery).
|
||||
|
||||
Actually sending and receiving the events happens in the `send_event()` and
|
||||
`receive_event()` functions. When calling either `dispatch()` or
|
||||
`audioMaster()`, the caller will oftentimes either pass along some kind of
|
||||
data structure through the void pointer function argument, or they expect the
|
||||
function's return value to be a pointer to some kind of struct provided by
|
||||
the plugin or host. The behaviour for reading from and writing into these
|
||||
void pointers and returning pointers to objects when needed is encapsulated
|
||||
in the `DispatchDataConverter` and `HostCallbackDataCovnerter` classes for
|
||||
the `dispatcher()` and `audioMaster()` functions respectively. For operations
|
||||
involving the plugin editor there is also some extra glue in
|
||||
`Vst2Bridge::dispatch_wrapper`. On the receiving end of the function calls,
|
||||
the `passthrough_event()` function which calls the callback functions and
|
||||
handles the marshalling between our data types created by the
|
||||
`*DataConverter` classes and the VST API's different pointer types. This
|
||||
behaviour is separated from `receive_event()` so we can handle MIDI events
|
||||
separately. This is needed because a select few 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.
|
||||
TODO: Rewrite this after the socket changes are done
|
||||
|
||||
Actually sending and receiving the events happens in the
|
||||
`EventHandler::send()` and `EventHandler::receive()` functions. When calling
|
||||
either `dispatch()` or `audioMaster()`, the caller will oftentimes either
|
||||
pass along some kind of data structure through the void pointer function
|
||||
argument, or they expect the function's return value to be a pointer to some
|
||||
kind of struct provided by the plugin or host. The behaviour for reading from
|
||||
and writing into these void pointers and returning pointers to objects when
|
||||
needed is encapsulated in the `DispatchDataConverter` and
|
||||
`HostCallbackDataCovnerter` classes for the `dispatcher()` and
|
||||
`audioMaster()` functions respectively. For operations involving the plugin
|
||||
editor there is also some extra glue in `Vst2Bridge::dispatch_wrapper`. On
|
||||
the receiving end of the function calls, the `passthrough_event()` function
|
||||
which calls the callback functions and handles the marshalling between our
|
||||
data types created by the `*DataConverter` classes and the VST API's
|
||||
different pointer types. This behaviour is separated from `receive_event()`
|
||||
so we can handle MIDI events separately. This is needed because a select few
|
||||
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
|
||||
over the sockets described above.
|
||||
|
||||
@@ -73,6 +73,7 @@ boost_filesystem_dep = dependency(
|
||||
static : with_static_boost
|
||||
)
|
||||
bitsery_dep = subproject('bitsery').get_variable('bitsery_dep')
|
||||
function2_dep = subproject('function2').get_variable('function2_dep')
|
||||
threads_dep = dependency('threads')
|
||||
tomlplusplus_dep = subproject('tomlplusplus', version : '2.1.0').get_variable('tomlplusplus_dep')
|
||||
# The built in threads dependency does not know how to handle winegcc
|
||||
@@ -92,6 +93,7 @@ shared_library(
|
||||
'src/common/configuration.cpp',
|
||||
'src/common/logging.cpp',
|
||||
'src/common/serialization.cpp',
|
||||
'src/common/communication.cpp',
|
||||
'src/common/utils.cpp',
|
||||
'src/plugin/host-process.cpp',
|
||||
'src/plugin/plugin.cpp',
|
||||
@@ -116,6 +118,7 @@ host_sources = [
|
||||
'src/common/configuration.cpp',
|
||||
'src/common/logging.cpp',
|
||||
'src/common/serialization.cpp',
|
||||
'src/common/communication.cpp',
|
||||
'src/common/utils.cpp',
|
||||
'src/wine-host/bridges/vst2.cpp',
|
||||
'src/wine-host/editor.cpp',
|
||||
@@ -139,6 +142,7 @@ executable(
|
||||
boost_dep,
|
||||
boost_filesystem_dep,
|
||||
bitsery_dep,
|
||||
function2_dep,
|
||||
tomlplusplus_dep,
|
||||
wine_threads_dep,
|
||||
xcb_dep
|
||||
@@ -156,6 +160,7 @@ executable(
|
||||
boost_dep,
|
||||
boost_filesystem_dep,
|
||||
bitsery_dep,
|
||||
function2_dep,
|
||||
tomlplusplus_dep,
|
||||
wine_threads_dep,
|
||||
xcb_dep
|
||||
@@ -201,6 +206,7 @@ if with_bitbridge
|
||||
boost_dep,
|
||||
boost_filesystem_dep,
|
||||
bitsery_dep,
|
||||
function2_dep,
|
||||
tomlplusplus_dep,
|
||||
wine_threads_dep,
|
||||
xcb_dep
|
||||
@@ -225,6 +231,7 @@ if with_bitbridge
|
||||
boost_dep,
|
||||
boost_filesystem_dep,
|
||||
bitsery_dep,
|
||||
function2_dep,
|
||||
tomlplusplus_dep,
|
||||
wine_threads_dep,
|
||||
xcb_dep
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// yabridge: a Wine VST bridge
|
||||
// Copyright (C) 2020 Robbert van der Helm
|
||||
//
|
||||
// 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "communication.h"
|
||||
|
||||
#include <random>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
/**
|
||||
* Used for generating random identifiers.
|
||||
*/
|
||||
constexpr char alphanumeric_characters[] =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
EventPayload DefaultDataConverter::read(const int /*opcode*/,
|
||||
const int /*index*/,
|
||||
const intptr_t /*value*/,
|
||||
const void* data) const {
|
||||
if (!data) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// This is a simple fallback that will work in almost every case.
|
||||
// Because some plugins don't zero out their string buffers when sending
|
||||
// host callbacks, we will explicitely list all callbacks that expect a
|
||||
// string in `DispatchDataConverter` adn `HostCallbackDataConverter`.
|
||||
const char* c_string = static_cast<const char*>(data);
|
||||
if (c_string[0] != 0) {
|
||||
return std::string(c_string);
|
||||
} else {
|
||||
return WantsString{};
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<EventPayload> DefaultDataConverter::read_value(
|
||||
const int /*opcode*/,
|
||||
const intptr_t /*value*/) const {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void DefaultDataConverter::write(const int /*opcode*/,
|
||||
void* data,
|
||||
const EventResult& response) const {
|
||||
// The default behavior is to handle this as a null terminated C-style
|
||||
// string
|
||||
std::visit(overload{[&](const auto&) {},
|
||||
[&](const std::string& s) {
|
||||
char* output = static_cast<char*>(data);
|
||||
|
||||
// We use std::string for easy transport but in
|
||||
// practice we're always writing null terminated
|
||||
// C-style strings
|
||||
std::copy(s.begin(), s.end(), output);
|
||||
output[s.size()] = 0;
|
||||
}},
|
||||
response.payload);
|
||||
}
|
||||
|
||||
void DefaultDataConverter::write_value(const int /*opcode*/,
|
||||
intptr_t /*value*/,
|
||||
const EventResult& /*response*/) const {}
|
||||
|
||||
intptr_t DefaultDataConverter::return_value(const int /*opcode*/,
|
||||
const intptr_t original) const {
|
||||
return original;
|
||||
}
|
||||
|
||||
boost::filesystem::path generate_endpoint_base(const std::string& plugin_name) {
|
||||
fs::path temp_directory = get_temporary_directory();
|
||||
|
||||
std::random_device random_device;
|
||||
std::mt19937 rng(random_device());
|
||||
fs::path candidate_endpoint;
|
||||
do {
|
||||
std::string random_id;
|
||||
std::sample(
|
||||
alphanumeric_characters,
|
||||
alphanumeric_characters + strlen(alphanumeric_characters) - 1,
|
||||
std::back_inserter(random_id), 8, rng);
|
||||
|
||||
// We'll get rid of the file descriptors immediately after accepting the
|
||||
// sockets, so putting them inside of a subdirectory would only leave
|
||||
// behind an empty directory
|
||||
std::ostringstream socket_name;
|
||||
socket_name << "yabridge-" << plugin_name << "-" << random_id;
|
||||
|
||||
candidate_endpoint = temp_directory / socket_name.str();
|
||||
} while (fs::exists(candidate_endpoint));
|
||||
|
||||
return candidate_endpoint;
|
||||
}
|
||||
+846
-1
@@ -16,15 +16,22 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <iostream>
|
||||
|
||||
#include <bitsery/adapter/buffer.h>
|
||||
#include <bitsery/bitsery.h>
|
||||
|
||||
#ifdef __WINE__
|
||||
#include "../wine-host/boost-fix.h"
|
||||
#endif
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/local/stream_protocol.hpp>
|
||||
#include <boost/asio/read.hpp>
|
||||
#include <boost/asio/write.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "logging.h"
|
||||
|
||||
template <typename B>
|
||||
using OutputAdapter = bitsery::OutputBufferAdapter<B>;
|
||||
@@ -108,9 +115,847 @@ inline T read_object(Socket& socket,
|
||||
{buffer.begin(), size}, object);
|
||||
|
||||
if (BOOST_UNLIKELY(!success)) {
|
||||
throw std::runtime_error("Deserialization failure in call:" +
|
||||
throw std::runtime_error("Deserialization failure in call: " +
|
||||
std::string(__PRETTY_FUNCTION__));
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* So, this is a bit of a mess. The TL;DR is that we want to use a single long
|
||||
* living socket connection for `dispatch()` and another one for `audioMaster()`
|
||||
* for performance reasons, but when the socket is already being written to we
|
||||
* create new connections on demand.
|
||||
*
|
||||
* For most of our sockets we can just send out our messages on the writing
|
||||
* side, and do a simple blocking loop on the reading side. The `dispatch()` and
|
||||
* `audioMaster()` calls are different. Not only do they have they come with
|
||||
* complex payload values, they can also be called simultaneously from multiple
|
||||
* threads, and `audioMaster()` and `dispatch()` calls can even be mutually
|
||||
* recursive. Luckily this does not happen very often, but it does mean that our
|
||||
* 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:
|
||||
*
|
||||
* - We'll keep a single long lived socket connection. This works the exact same
|
||||
* way as every other socket defined in the `Sockets` class.
|
||||
* - Aside from that the listening side will have a second thread asynchronously
|
||||
* listening for new connections on the socket endpoint.
|
||||
*
|
||||
* 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
|
||||
* above. Similarly, the `EventHandler::receive()` method first sets up
|
||||
* asynchronous listeners for the socket endpoint, and then block and handle
|
||||
* events until the main socket is closed.
|
||||
*
|
||||
* @tparam Thread The thread implementation to use. On the Linux side this
|
||||
* should be `std::jthread` and on the Wine side this should be `Win32Thread`.
|
||||
*/
|
||||
template <typename Thread>
|
||||
class EventHandler {
|
||||
public:
|
||||
/**
|
||||
* Sets up a single main socket for this type of events. The sockets won't
|
||||
* be active until `connect()` gets called.
|
||||
*
|
||||
* @param io_context The IO context the main socket should be bound to. A
|
||||
* new IO context will be created for accepting the additional incoming
|
||||
* connections.
|
||||
* @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)
|
||||
: io_context(io_context), endpoint(endpoint), socket(io_context) {
|
||||
if (listen) {
|
||||
boost::filesystem::create_directories(
|
||||
boost::filesystem::path(endpoint.path()).parent_path());
|
||||
acceptor.emplace(io_context, endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
if (acceptor) {
|
||||
acceptor->accept(socket);
|
||||
|
||||
// As mentioned in `acceptor's` docstring, this acceptor will be
|
||||
// recreated in `receive()` on another context, and potentially on
|
||||
// the other side of the connection in the case of
|
||||
// `vst_host_callback`
|
||||
acceptor.reset();
|
||||
boost::filesystem::remove(endpoint.path());
|
||||
} else {
|
||||
socket.connect(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the socket. Both sides that are actively listening will be thrown a
|
||||
* `boost::system_error` when this happens.
|
||||
*/
|
||||
void close() {
|
||||
// The shutdown can fail when the socket is already closed
|
||||
boost::system::error_code err;
|
||||
socket.shutdown(
|
||||
boost::asio::local::stream_protocol::socket::shutdown_both, err);
|
||||
socket.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) {
|
||||
// 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};
|
||||
|
||||
// A socket only handles a single request at a time as to prevent
|
||||
// messages from arriving out of order. For throughput reasons we prefer
|
||||
// to do most communication over a single main socket (`socket`), and
|
||||
// we'll lock `write_mutex` while doing so. In the event that the mutex
|
||||
// is already locked and thus the main socket is currently in use by
|
||||
// another thread, then we'll spawn a new socket to handle the request.
|
||||
EventResult response;
|
||||
{
|
||||
std::unique_lock lock(write_mutex, std::try_to_lock);
|
||||
if (lock.owns_lock()) {
|
||||
write_object(socket, event);
|
||||
response = read_object<EventResult>(socket);
|
||||
} else {
|
||||
try {
|
||||
boost::asio::local::stream_protocol::socket
|
||||
secondary_socket(io_context);
|
||||
secondary_socket.connect(endpoint);
|
||||
|
||||
write_object(secondary_socket, event);
|
||||
response = read_object<EventResult>(secondary_socket);
|
||||
} catch (const boost::system::system_error&) {
|
||||
// So, what do we do when noone is listening on the endpoint
|
||||
// yet? This can happen with plugin groups when the Wine
|
||||
// host process does an `audioMaster()` call before the
|
||||
// plugin is listening. If that happens we'll fall back to a
|
||||
// synchronous request. This is not very pretty, so if
|
||||
// anyone can think of a better way to structure all of this
|
||||
// while still mainting a long living primary socket please
|
||||
// let me know.
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a new thread to listen for extra connections to `endpoint`, and
|
||||
* then a blocking loop that handles events from the primary `socket`.
|
||||
*
|
||||
* The specified function will be used to create an `EventResult` from an
|
||||
* `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.
|
||||
* See the definition of `F` for more information.
|
||||
*
|
||||
* @tparam F A function type in the form of `EventResponse(Event, bool)`.
|
||||
* The boolean flag is `true` when this event was received on the main
|
||||
* socket, and `false` otherwise.
|
||||
*
|
||||
* @relates EventHandler::send
|
||||
* @relates passthrough_event
|
||||
*/
|
||||
template <typename F>
|
||||
void receive(std::optional<std::pair<Logger&, bool>> logging, F callback) {
|
||||
// As described above we'll handle incoming requests for `socket` on
|
||||
// this thread. We'll also listen for incoming connections on `endpoint`
|
||||
// on another thread. For any incoming connection we'll spawn a new
|
||||
// thread to handle the request. When `socket` closes and this loop
|
||||
// breaks, the listener and any still active threads will be cleaned up
|
||||
// before this function exits.
|
||||
boost::asio::io_context secondary_context{};
|
||||
|
||||
// The previous acceptor has already been shut down by
|
||||
// `EventHandler::connect()`
|
||||
acceptor.emplace(secondary_context, endpoint);
|
||||
|
||||
// This works the exact same was as `active_plugins` and
|
||||
// `next_plugin_id` in `GroupBridge`
|
||||
std::map<size_t, Thread> active_secondary_requests{};
|
||||
std::atomic_size_t next_request_id{};
|
||||
std::mutex active_secondary_requests_mutex{};
|
||||
accept_requests(
|
||||
*acceptor, logging,
|
||||
[&](boost::asio::local::stream_protocol::socket secondary_socket) {
|
||||
const size_t request_id = next_request_id.fetch_add(1);
|
||||
|
||||
// We have to make sure to keep moving these sockets into the
|
||||
// threads that will handle them
|
||||
std::lock_guard lock(active_secondary_requests_mutex);
|
||||
active_secondary_requests[request_id] = Thread(
|
||||
[&, request_id](boost::asio::local::stream_protocol::socket
|
||||
secondary_socket) {
|
||||
// TODO: Factor this out
|
||||
auto event = read_object<Event>(secondary_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, false);
|
||||
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(secondary_socket, response);
|
||||
|
||||
// When we have processed this request, we'll join the
|
||||
// thread again with the thread that's handling
|
||||
// `secondary_context`.
|
||||
boost::asio::post(secondary_context, [&, request_id]() {
|
||||
std::lock_guard lock(
|
||||
active_secondary_requests_mutex);
|
||||
|
||||
// The join is implicit because we're using
|
||||
// std::jthread/Win32Thread
|
||||
active_secondary_requests.erase(request_id);
|
||||
});
|
||||
},
|
||||
std::move(secondary_socket));
|
||||
});
|
||||
|
||||
Thread secondary_requests_handler([&]() { secondary_context.run(); });
|
||||
|
||||
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, true);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// After the main socket gets terminated (during shutdown) we'll make
|
||||
// sure all outstanding jobs have been processed and then drop all work
|
||||
// from the IO context
|
||||
std::lock_guard lock(active_secondary_requests_mutex);
|
||||
secondary_context.stop();
|
||||
acceptor.reset();
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* Used in `receive()` to asynchronously listen for secondary socket
|
||||
* connections. After `callback()` returns this function will continue to be
|
||||
* called until the IO context gets stopped.
|
||||
*
|
||||
* @param acceptor The acceptor we will be listening on.
|
||||
* @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 A function that handles the new socket connection.
|
||||
*
|
||||
* @tparam F A function in the form
|
||||
* `void(boost::asio::local::stream_protocol::socket)` to handle a new
|
||||
* incoming connection.
|
||||
*/
|
||||
template <typename F>
|
||||
void accept_requests(
|
||||
boost::asio::local::stream_protocol::acceptor& acceptor,
|
||||
std::optional<std::pair<Logger&, bool>> logging,
|
||||
F callback) {
|
||||
acceptor.async_accept(
|
||||
[&, logging, callback](
|
||||
const boost::system::error_code& error,
|
||||
boost::asio::local::stream_protocol::socket secondary_socket) {
|
||||
if (error.failed()) {
|
||||
// On the Wine side it's expected that the main socket
|
||||
// connection will be dropped during shutdown, so we can
|
||||
// silently ignore any related socket errors on the Wine
|
||||
// side
|
||||
if (logging) {
|
||||
auto [logger, is_dispatch] = *logging;
|
||||
logger.log("Failure while accepting connections: " +
|
||||
error.message());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
callback(std::move(secondary_socket));
|
||||
|
||||
accept_requests(acceptor, logging, callback);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The main IO context. New sockets created during `send()` will be bound to
|
||||
* this context. In `receive()` we'll create a new IO context since we want
|
||||
* to do all listening there on a dedicated thread.
|
||||
*/
|
||||
boost::asio::io_context& io_context;
|
||||
|
||||
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 then
|
||||
* called, we'll recreate the acceptor asynchronously listen for new
|
||||
* incoming socket connections on `endpoint` using this same acceptor. This
|
||||
* is important, because on the case of `vst_host_callback` the acceptor is
|
||||
* first accepts an initial socket on the plugin side (like all sockets),
|
||||
* but all additional incoming connections of course have to be listened for
|
||||
* on the plugin side.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @tparam Thread The thread implementation to use. On the Linux side this
|
||||
* should be `std::jthread` and on the Wine side this should be `Win32Thread`.
|
||||
*/
|
||||
template <typename Thread>
|
||||
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)
|
||||
: 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) {
|
||||
boost::filesystem::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},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the directory containing the socket endpoints when yabridge
|
||||
* shuts down if it still exists.
|
||||
*/
|
||||
~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 {
|
||||
boost::filesystem::remove_all(base_dir);
|
||||
} catch (const boost::filesystem::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
|
||||
}
|
||||
}
|
||||
|
||||
// Manually close all sockets so we break out of any blocking operations
|
||||
// that may still be active
|
||||
host_vst_dispatch.close();
|
||||
host_vst_dispatch_midi_events.close();
|
||||
vst_host_callback.close();
|
||||
|
||||
// These shutdowns can fail when the socket has already been closed, but
|
||||
// that's not an issue in our case
|
||||
constexpr auto shutdown_type =
|
||||
boost::asio::local::stream_protocol::socket::shutdown_both;
|
||||
boost::system::error_code err;
|
||||
host_vst_parameters.shutdown(shutdown_type, err);
|
||||
host_vst_process_replacing.shutdown(shutdown_type, err);
|
||||
host_vst_control.shutdown(shutdown_type, err);
|
||||
host_vst_parameters.close();
|
||||
host_vst_process_replacing.close();
|
||||
host_vst_control.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Thread> 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<Thread> host_vst_dispatch_midi_events;
|
||||
/**
|
||||
* The socket that forwards all `audioMaster()` calls from the Windows VST
|
||||
* plugin to the host.
|
||||
*/
|
||||
EventHandler<Thread> 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
|
||||
* into the expected format for VST2 function calls, calls the given function
|
||||
* (either `AEffect::dispatcher()` for host -> plugin events or `audioMaster()`
|
||||
* for plugin -> host events), and serializes the results back into an
|
||||
* `EventResult` object. I'd rather not get too Haskell-y in my C++, but this is
|
||||
* the cleanest solution for this problem.
|
||||
*
|
||||
* This is the receiving analogue of the `*DataCovnerter` objects.
|
||||
*
|
||||
* TODO: Now that `EventHandler::receive` replaced `receive_event()`, refactor
|
||||
* this to just handle the event directly rather than returning a lambda
|
||||
*
|
||||
* @param plugin The `AEffect` instance that should be passed to the callback
|
||||
* function.
|
||||
* @param callback The function to call with the arguments received from the
|
||||
* socket.
|
||||
*
|
||||
* @tparam A function with the same signature as `AEffect::dispatcher` or
|
||||
* `audioMasterCallback`.
|
||||
*
|
||||
* @return A `EventResult(Event)` callback function that can be passed to
|
||||
* `EditorHandler::receive()`.
|
||||
*
|
||||
* @relates EditorHandler::receive
|
||||
*/
|
||||
template <typename F>
|
||||
auto passthrough_event(AEffect* plugin, F callback) {
|
||||
return [=](Event& event) -> EventResult {
|
||||
// This buffer is used to write strings and small objects to. We'll
|
||||
// initialize the beginning with null values to both prevent it from
|
||||
// being read as some arbitrary C-style string, and to make sure that
|
||||
// `*static_cast<void**>(string_buffer.data)` will be a null pointer if
|
||||
// the plugin is supposed to write a pointer there but doesn't (such as
|
||||
// with `effEditGetRect`/`WantsVstRect`).
|
||||
std::array<char, max_string_length> string_buffer;
|
||||
std::fill(string_buffer.begin(), string_buffer.begin() + sizeof(size_t),
|
||||
0);
|
||||
|
||||
auto read_payload_fn = overload{
|
||||
[&](const std::nullptr_t&) -> void* { return nullptr; },
|
||||
[&](const std::string& s) -> void* {
|
||||
return const_cast<char*>(s.c_str());
|
||||
},
|
||||
[&](const std::vector<uint8_t>& buffer) -> void* {
|
||||
return const_cast<uint8_t*>(buffer.data());
|
||||
},
|
||||
[&](native_size_t& window_handle) -> void* {
|
||||
// This is the X11 window handle that the editor should reparent
|
||||
// itself to. We have a special wrapper around the dispatch
|
||||
// function that intercepts `effEditOpen` events and creates a
|
||||
// Win32 window and then finally embeds the X11 window Wine
|
||||
// created into this wnidow handle. Make sure to convert the
|
||||
// window ID first to `size_t` in case this is the 32-bit host.
|
||||
return reinterpret_cast<void*>(
|
||||
static_cast<size_t>(window_handle));
|
||||
},
|
||||
[&](const AEffect&) -> void* { return nullptr; },
|
||||
[&](DynamicVstEvents& events) -> void* {
|
||||
return &events.as_c_events();
|
||||
},
|
||||
[&](DynamicSpeakerArrangement& speaker_arrangement) -> void* {
|
||||
return &speaker_arrangement.as_c_speaker_arrangement();
|
||||
},
|
||||
[&](WantsAEffectUpdate&) -> void* {
|
||||
// The host will never actually ask for an updated `AEffect`
|
||||
// object since that should not be a thing. This is purely a
|
||||
// meant as a workaround for plugins that initialize their
|
||||
// `AEffect` object after the plugin has already finished
|
||||
// initializing.
|
||||
return nullptr;
|
||||
},
|
||||
[&](WantsChunkBuffer&) -> void* { return string_buffer.data(); },
|
||||
[&](VstIOProperties& props) -> void* { return &props; },
|
||||
[&](VstMidiKeyName& key_name) -> void* { return &key_name; },
|
||||
[&](VstParameterProperties& props) -> void* { return &props; },
|
||||
[&](WantsVstRect&) -> void* { return string_buffer.data(); },
|
||||
[&](const WantsVstTimeInfo&) -> void* { return nullptr; },
|
||||
[&](WantsString&) -> void* { return string_buffer.data(); }};
|
||||
|
||||
// Almost all events pass data through the `data` argument. There are
|
||||
// two events, `effSetParameter` and `effGetParameter` that also pass
|
||||
// data through the value argument.
|
||||
void* data = std::visit(read_payload_fn, event.payload);
|
||||
intptr_t value = event.value;
|
||||
if (event.value_payload) {
|
||||
value = reinterpret_cast<intptr_t>(
|
||||
std::visit(read_payload_fn, *event.value_payload));
|
||||
}
|
||||
|
||||
const intptr_t return_value = callback(
|
||||
plugin, event.opcode, event.index, value, data, event.option);
|
||||
|
||||
// Only write back data when needed, this depends on the event payload
|
||||
// type
|
||||
auto write_payload_fn = overload{
|
||||
[&](auto) -> EventResultPayload { return nullptr; },
|
||||
[&](const AEffect& updated_plugin) -> EventResultPayload {
|
||||
// This is a bit of a special case! Instead of writing some
|
||||
// return value, we will update values on the native VST
|
||||
// plugin's `AEffect` object. This is triggered by the
|
||||
// `audioMasterIOChanged` callback from the hosted VST plugin.
|
||||
update_aeffect(*plugin, updated_plugin);
|
||||
|
||||
return nullptr;
|
||||
},
|
||||
[&](DynamicSpeakerArrangement& speaker_arrangement)
|
||||
-> EventResultPayload { return speaker_arrangement; },
|
||||
[&](WantsChunkBuffer&) -> EventResultPayload {
|
||||
// In this case the plugin will have written its data stored in
|
||||
// an array to which a pointer is stored in `data`, with the
|
||||
// return value from the event determines how much data the
|
||||
// plugin has written
|
||||
const uint8_t* chunk_data = *static_cast<uint8_t**>(data);
|
||||
return std::vector<uint8_t>(chunk_data,
|
||||
chunk_data + return_value);
|
||||
},
|
||||
[&](VstIOProperties& props) -> EventResultPayload { return props; },
|
||||
[&](VstMidiKeyName& key_name) -> EventResultPayload {
|
||||
return key_name;
|
||||
},
|
||||
[&](VstParameterProperties& props) -> EventResultPayload {
|
||||
return props;
|
||||
},
|
||||
[&](WantsAEffectUpdate&) -> EventResultPayload { return *plugin; },
|
||||
[&](WantsVstRect&) -> EventResultPayload {
|
||||
// The plugin should have written a pointer to a VstRect struct
|
||||
// into the data pointer. I haven't seen this fail yet, but
|
||||
// since some hosts will call `effEditGetRect()` before
|
||||
// `effEditOpen()` I can assume there are plugins that don't
|
||||
// handle this correctly.
|
||||
VstRect* editor_rect = *static_cast<VstRect**>(data);
|
||||
if (!editor_rect) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return *editor_rect;
|
||||
},
|
||||
[&](WantsVstTimeInfo&) -> EventResultPayload {
|
||||
// Not sure why the VST API has twenty different ways of
|
||||
// returning structs, but in this case the value returned from
|
||||
// the callback function is actually a pointer to a
|
||||
// `VstTimeInfo` struct! It can also be a null pointer if the
|
||||
// host doesn't support this.
|
||||
const auto time_info =
|
||||
reinterpret_cast<const VstTimeInfo*>(return_value);
|
||||
if (!time_info) {
|
||||
return nullptr;
|
||||
} else {
|
||||
return *time_info;
|
||||
}
|
||||
},
|
||||
[&](WantsString&) -> EventResultPayload {
|
||||
return std::string(static_cast<char*>(data));
|
||||
}};
|
||||
|
||||
// As mentioned about, the `effSetSpeakerArrangement` and
|
||||
// `effGetSpeakerArrangement` events are the only two events that use
|
||||
// the value argument as a pointer to write data to. Additionally, the
|
||||
// `effGetSpeakerArrangement` expects the plugin to write its own data
|
||||
// to this value. Hence why we need to encode the response here
|
||||
// separately.
|
||||
const EventResultPayload response_data =
|
||||
std::visit(write_payload_fn, event.payload);
|
||||
std::optional<EventResultPayload> value_response_data = std::nullopt;
|
||||
if (event.value_payload) {
|
||||
value_response_data =
|
||||
std::visit(write_payload_fn, *event.value_payload);
|
||||
}
|
||||
|
||||
EventResult response{.return_value = return_value,
|
||||
.payload = response_data,
|
||||
.value_payload = value_response_data};
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,12 +84,6 @@ Configuration::Configuration(const fs::path& config_path,
|
||||
} else {
|
||||
invalid_options.push_back(key);
|
||||
}
|
||||
} else if (key == "hack_reaper_update_display") {
|
||||
if (const auto parsed_value = value.as_boolean()) {
|
||||
hack_reaper_update_display = parsed_value->get();
|
||||
} else {
|
||||
invalid_options.push_back(key);
|
||||
}
|
||||
} else if (key == "group") {
|
||||
if (const auto parsed_value = value.as_string()) {
|
||||
group = parsed_value->get();
|
||||
|
||||
@@ -87,14 +87,6 @@ class Configuration {
|
||||
*/
|
||||
bool editor_double_embed = false;
|
||||
|
||||
/**
|
||||
* If this is set to true, then any calls to `audioMasterUpdateDisplay()`
|
||||
* will automatically return 0 without being sent to the host. This is a
|
||||
* HACK to work around implementations issues in REAPER and Renoise, see #29
|
||||
* and #32.
|
||||
*/
|
||||
bool hack_reaper_update_display = false;
|
||||
|
||||
/**
|
||||
* The name of the plugin group that should be used for the plugin this
|
||||
* configuration object was created for. If not set, then the plugin should
|
||||
@@ -128,7 +120,6 @@ class Configuration {
|
||||
template <typename S>
|
||||
void serialize(S& s) {
|
||||
s.value1b(editor_double_embed);
|
||||
s.value1b(hack_reaper_update_display);
|
||||
s.ext(group, bitsery::ext::StdOptional(),
|
||||
[](S& s, auto& v) { s.text1b(v, 4096); });
|
||||
s.ext(matched_file, bitsery::ext::StdOptional(),
|
||||
|
||||
@@ -1,408 +0,0 @@
|
||||
// yabridge: a Wine VST bridge
|
||||
// Copyright (C) 2020 Robbert van der Helm
|
||||
//
|
||||
// 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "communication.h"
|
||||
#include "logging.h"
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (!data) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// This is a simple fallback that will work in almost every case.
|
||||
// Because some plugins don't zero out their string buffers when sending
|
||||
// host callbacks, we will explicitely list all callbacks that expect a
|
||||
// string in `DispatchDataConverter` adn `HostCallbackDataConverter`.
|
||||
const char* c_string = static_cast<const char*>(data);
|
||||
if (c_string[0] != 0) {
|
||||
return std::string(c_string);
|
||||
} else {
|
||||
return WantsString{};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the reponse back to the `data` pointer.
|
||||
*/
|
||||
virtual void write(const int /*opcode*/,
|
||||
void* data,
|
||||
const EventResult& response) const {
|
||||
// The default behavior is to handle this as a null terminated C-style
|
||||
// string
|
||||
std::visit(overload{[&](const auto&) {},
|
||||
[&](const std::string& s) {
|
||||
char* output = static_cast<char*>(data);
|
||||
|
||||
// We use std::string for easy transport but in
|
||||
// practice we're always writing null terminated
|
||||
// C-style strings
|
||||
std::copy(s.begin(), s.end(), output);
|
||||
output[s.size()] = 0;
|
||||
}},
|
||||
response.payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return original;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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>
|
||||
intptr_t send_event(boost::asio::local::stream_protocol::socket& socket,
|
||||
std::mutex& write_mutex,
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive an event from a socket, call a function to generate a response, and
|
||||
* write the response back over the socket. This is usually used together with
|
||||
* `passthrough_event()` which passes the event data through to an event
|
||||
* dispatcher function. This behaviour is split into two functions to avoid
|
||||
* redundant data conversions when handling MIDI data, as some plugins require
|
||||
* the received data to be temporarily stored until the next event audio buffer
|
||||
* gets processed.
|
||||
*
|
||||
* @param socket The socket to receive on and to send the response back to.
|
||||
* @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 send_event
|
||||
* @relates passthrough_event
|
||||
*/
|
||||
template <typename F>
|
||||
void receive_event(boost::asio::local::stream_protocol::socket& socket,
|
||||
std::optional<std::pair<Logger&, bool>> logging,
|
||||
F callback) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a callback function that takes an `Event` object, decodes the data
|
||||
* into the expected format for VST2 function calls, calls the given function
|
||||
* (either `AEffect::dispatcher()` for host -> plugin events or `audioMaster()`
|
||||
* for plugin -> host events), and serializes the results back into an
|
||||
* `EventResult` object. I'd rather not get too Haskell-y in my C++, but this is
|
||||
* the cleanest solution for this problem.
|
||||
*
|
||||
* This is the receiving analogue of the `*DataCovnerter` objects.
|
||||
*
|
||||
* @param plugin The `AEffect` instance that should be passed to the callback
|
||||
* function.
|
||||
* @param callback The function to call with the arguments received from the
|
||||
* socket.
|
||||
*
|
||||
* @tparam A function with the same signature as `AEffect::dispatcher` or
|
||||
* `audioMasterCallback`.
|
||||
*
|
||||
* @return A `EventResult(Event)` callback function that can be passed to
|
||||
* `receive_event`.
|
||||
*
|
||||
* @relates receive_event
|
||||
*/
|
||||
template <typename F>
|
||||
auto passthrough_event(AEffect* plugin, F callback) {
|
||||
return [=](Event& event) -> EventResult {
|
||||
// This buffer is used to write strings and small objects to. We'll
|
||||
// initialize the beginning with null values to both prevent it from
|
||||
// being read as some arbitrary C-style string, and to make sure that
|
||||
// `*static_cast<void**>(string_buffer.data)` will be a null pointer if
|
||||
// the plugin is supposed to write a pointer there but doesn't (such as
|
||||
// with `effEditGetRect`/`WantsVstRect`).
|
||||
std::array<char, max_string_length> string_buffer;
|
||||
std::fill(string_buffer.begin(), string_buffer.begin() + sizeof(size_t),
|
||||
0);
|
||||
|
||||
auto read_payload_fn = overload{
|
||||
[&](const std::nullptr_t&) -> void* { return nullptr; },
|
||||
[&](const std::string& s) -> void* {
|
||||
return const_cast<char*>(s.c_str());
|
||||
},
|
||||
[&](const std::vector<uint8_t>& buffer) -> void* {
|
||||
return const_cast<uint8_t*>(buffer.data());
|
||||
},
|
||||
[&](native_size_t& window_handle) -> void* {
|
||||
// This is the X11 window handle that the editor should reparent
|
||||
// itself to. We have a special wrapper around the dispatch
|
||||
// function that intercepts `effEditOpen` events and creates a
|
||||
// Win32 window and then finally embeds the X11 window Wine
|
||||
// created into this wnidow handle. Make sure to convert the
|
||||
// window ID first to `size_t` in case this is the 32-bit host.
|
||||
return reinterpret_cast<void*>(
|
||||
static_cast<size_t>(window_handle));
|
||||
},
|
||||
[&](const AEffect&) -> void* { return nullptr; },
|
||||
[&](DynamicVstEvents& events) -> void* {
|
||||
return &events.as_c_events();
|
||||
},
|
||||
[&](DynamicSpeakerArrangement& speaker_arrangement) -> void* {
|
||||
return &speaker_arrangement.as_c_speaker_arrangement();
|
||||
},
|
||||
[&](WantsAEffectUpdate&) -> void* {
|
||||
// The host will never actually ask for an updated `AEffect`
|
||||
// object since that should not be a thing. This is purely a
|
||||
// meant as a workaround for plugins that initialize their
|
||||
// `AEffect` object after the plugin has already finished
|
||||
// initializing.
|
||||
return nullptr;
|
||||
},
|
||||
[&](WantsChunkBuffer&) -> void* { return string_buffer.data(); },
|
||||
[&](VstIOProperties& props) -> void* { return &props; },
|
||||
[&](VstMidiKeyName& key_name) -> void* { return &key_name; },
|
||||
[&](VstParameterProperties& props) -> void* { return &props; },
|
||||
[&](WantsVstRect&) -> void* { return string_buffer.data(); },
|
||||
[&](const WantsVstTimeInfo&) -> void* { return nullptr; },
|
||||
[&](WantsString&) -> void* { return string_buffer.data(); }};
|
||||
|
||||
// Almost all events pass data through the `data` argument. There are
|
||||
// two events, `effSetParameter` and `effGetParameter` that also pass
|
||||
// data through the value argument.
|
||||
void* data = std::visit(read_payload_fn, event.payload);
|
||||
intptr_t value = event.value;
|
||||
if (event.value_payload) {
|
||||
value = reinterpret_cast<intptr_t>(
|
||||
std::visit(read_payload_fn, *event.value_payload));
|
||||
}
|
||||
|
||||
const intptr_t return_value = callback(
|
||||
plugin, event.opcode, event.index, value, data, event.option);
|
||||
|
||||
// Only write back data when needed, this depends on the event payload
|
||||
// type
|
||||
auto write_payload_fn = overload{
|
||||
[&](auto) -> EventResultPayload { return nullptr; },
|
||||
[&](const AEffect& updated_plugin) -> EventResultPayload {
|
||||
// This is a bit of a special case! Instead of writing some
|
||||
// return value, we will update values on the native VST
|
||||
// plugin's `AEffect` object. This is triggered by the
|
||||
// `audioMasterIOChanged` callback from the hosted VST plugin.
|
||||
update_aeffect(*plugin, updated_plugin);
|
||||
|
||||
return nullptr;
|
||||
},
|
||||
[&](DynamicSpeakerArrangement& speaker_arrangement)
|
||||
-> EventResultPayload { return speaker_arrangement; },
|
||||
[&](WantsChunkBuffer&) -> EventResultPayload {
|
||||
// In this case the plugin will have written its data stored in
|
||||
// an array to which a pointer is stored in `data`, with the
|
||||
// return value from the event determines how much data the
|
||||
// plugin has written
|
||||
const uint8_t* chunk_data = *static_cast<uint8_t**>(data);
|
||||
return std::vector<uint8_t>(chunk_data,
|
||||
chunk_data + return_value);
|
||||
},
|
||||
[&](VstIOProperties& props) -> EventResultPayload { return props; },
|
||||
[&](VstMidiKeyName& key_name) -> EventResultPayload {
|
||||
return key_name;
|
||||
},
|
||||
[&](VstParameterProperties& props) -> EventResultPayload {
|
||||
return props;
|
||||
},
|
||||
[&](WantsAEffectUpdate&) -> EventResultPayload { return *plugin; },
|
||||
[&](WantsVstRect&) -> EventResultPayload {
|
||||
// The plugin should have written a pointer to a VstRect struct
|
||||
// into the data pointer. I haven't seen this fail yet, but
|
||||
// since some hosts will call `effEditGetRect()` before
|
||||
// `effEditOpen()` I can assume there are plugins that don't
|
||||
// handle this correctly.
|
||||
VstRect* editor_rect = *static_cast<VstRect**>(data);
|
||||
if (!editor_rect) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return *editor_rect;
|
||||
},
|
||||
[&](WantsVstTimeInfo&) -> EventResultPayload {
|
||||
// Not sure why the VST API has twenty different ways of
|
||||
// returning structs, but in this case the value returned from
|
||||
// the callback function is actually a pointer to a
|
||||
// `VstTimeInfo` struct! It can also be a null pointer if the
|
||||
// host doesn't support this.
|
||||
const auto time_info =
|
||||
reinterpret_cast<const VstTimeInfo*>(return_value);
|
||||
if (!time_info) {
|
||||
return nullptr;
|
||||
} else {
|
||||
return *time_info;
|
||||
}
|
||||
},
|
||||
[&](WantsString&) -> EventResultPayload {
|
||||
return std::string(static_cast<char*>(data));
|
||||
}};
|
||||
|
||||
// As mentioned about, the `effSetSpeakerArrangement` and
|
||||
// `effGetSpeakerArrangement` events are the only two events that use
|
||||
// the value argument as a pointer to write data to. Additionally, the
|
||||
// `effGetSpeakerArrangement` expects the plugin to write its own data
|
||||
// to this value. Hence why we need to encode the response here
|
||||
// separately.
|
||||
const EventResultPayload response_data =
|
||||
std::visit(write_payload_fn, event.payload);
|
||||
std::optional<EventResultPayload> value_response_data = std::nullopt;
|
||||
if (event.value_payload) {
|
||||
value_response_data =
|
||||
std::visit(write_payload_fn, *event.value_payload);
|
||||
}
|
||||
|
||||
EventResult response{.return_value = return_value,
|
||||
.payload = response_data,
|
||||
.value_payload = value_response_data};
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
@@ -97,8 +97,9 @@ class Logger {
|
||||
void log_get_parameter_response(float vlaue);
|
||||
void log_set_parameter(int index, float value);
|
||||
void log_set_parameter_response();
|
||||
// If is_dispatch is true, then use opcode names from the plugin's dispatch
|
||||
// function. Otherwise use names for the host callback function opcodes.
|
||||
// If `is_dispatch` is `true`, then use opcode names from the plugin's
|
||||
// dispatch function. Otherwise use names for the host callback function
|
||||
// opcodes.
|
||||
void log_event(bool is_dispatch,
|
||||
int opcode,
|
||||
int index,
|
||||
|
||||
@@ -108,7 +108,3 @@ AEffect& update_aeffect(AEffect& plugin, const AEffect& updated_plugin) {
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
bool GroupRequest::operator==(const GroupRequest& rhs) const {
|
||||
return plugin_path == rhs.plugin_path && socket_path == rhs.socket_path;
|
||||
}
|
||||
|
||||
@@ -598,14 +598,12 @@ struct AudioBuffers {
|
||||
*/
|
||||
struct GroupRequest {
|
||||
std::string plugin_path;
|
||||
std::string socket_path;
|
||||
|
||||
bool operator==(const GroupRequest& rhs) const;
|
||||
std::string endpoint_base_dir;
|
||||
|
||||
template <typename S>
|
||||
void serialize(S& s) {
|
||||
s.text1b(plugin_path, 4096);
|
||||
s.text1b(socket_path, 4096);
|
||||
s.text1b(endpoint_base_dir, 4096);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -614,7 +612,8 @@ struct std::hash<GroupRequest> {
|
||||
std::size_t operator()(GroupRequest const& params) const noexcept {
|
||||
std::hash<string> hasher{};
|
||||
|
||||
return hasher(params.plugin_path) ^ (hasher(params.socket_path) << 1);
|
||||
return hasher(params.plugin_path) ^
|
||||
(hasher(params.endpoint_base_dir) << 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,19 @@
|
||||
#include "utils.h"
|
||||
|
||||
#include <sched.h>
|
||||
#include <boost/process/environment.hpp>
|
||||
|
||||
namespace bp = boost::process;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
fs::path get_temporary_directory() {
|
||||
bp::environment env = boost::this_process::environment();
|
||||
if (!env["XDG_RUNTIME_DIR"].empty()) {
|
||||
return env["XDG_RUNTIME_DIR"].to_string();
|
||||
} else {
|
||||
return fs::temp_directory_path();
|
||||
}
|
||||
}
|
||||
|
||||
bool set_realtime_priority() {
|
||||
sched_param params{.sched_priority = 5};
|
||||
|
||||
@@ -16,6 +16,17 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __WINE__
|
||||
#include "../wine-host/boost-fix.h"
|
||||
#endif
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
/**
|
||||
* Return the path to the directory for story temporary files. This will be
|
||||
* `$XDG_RUNTIME_DIR` if set, and `/tmp` otherwise.
|
||||
*/
|
||||
boost::filesystem::path get_temporary_directory();
|
||||
|
||||
/**
|
||||
* Set the scheduling policy to `SCHED_FIFO` with priority 10 for this process.
|
||||
* We explicitly don't do this for wineserver itself since from my testing that
|
||||
|
||||
+19
-20
@@ -83,7 +83,7 @@ void HostProcess::async_log_pipe_lines(patched_async_pipe& pipe,
|
||||
IndividualHost::IndividualHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
fs::path plugin_path,
|
||||
fs::path socket_endpoint)
|
||||
const Sockets<std::jthread>& sockets)
|
||||
: HostProcess(io_context, logger),
|
||||
plugin_arch(find_vst_architecture(plugin_path)),
|
||||
host_path(find_vst_host(plugin_arch, false)),
|
||||
@@ -93,7 +93,7 @@ IndividualHost::IndividualHost(boost::asio::io_context& io_context,
|
||||
#else
|
||||
plugin_path,
|
||||
#endif
|
||||
socket_endpoint,
|
||||
sockets.base_dir,
|
||||
bp::env = set_wineprefix(),
|
||||
bp::std_out = stdout_pipe,
|
||||
bp::std_err = stderr_pipe
|
||||
@@ -104,7 +104,7 @@ IndividualHost::IndividualHost(boost::asio::io_context& io_context,
|
||||
#endif
|
||||
)) {
|
||||
#ifdef WITH_WINEDBG
|
||||
if (plugin_path.string().find(' ') != std::string::npos) {
|
||||
if (plugin_path.filename().string().find(' ') != std::string::npos) {
|
||||
logger.log("Warning: winedbg does not support paths containing spaces");
|
||||
}
|
||||
#endif
|
||||
@@ -127,17 +127,15 @@ void IndividualHost::terminate() {
|
||||
host.wait();
|
||||
}
|
||||
|
||||
GroupHost::GroupHost(
|
||||
boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
fs::path plugin_path,
|
||||
fs::path socket_endpoint,
|
||||
std::string group_name,
|
||||
boost::asio::local::stream_protocol::socket& host_vst_dispatch)
|
||||
GroupHost::GroupHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
fs::path plugin_path,
|
||||
Sockets<std::jthread>& sockets,
|
||||
std::string group_name)
|
||||
: HostProcess(io_context, logger),
|
||||
plugin_arch(find_vst_architecture(plugin_path)),
|
||||
host_path(find_vst_host(plugin_arch, true)),
|
||||
host_vst_dispatch(host_vst_dispatch) {
|
||||
sockets(sockets) {
|
||||
#ifdef WITH_WINEDBG
|
||||
if (plugin_path.string().find(' ') != std::string::npos) {
|
||||
logger.log("Warning: winedbg does not support paths containing spaces");
|
||||
@@ -167,6 +165,7 @@ GroupHost::GroupHost(
|
||||
wine_prefix = fs::path(host_env.at("HOME").to_string()) / ".wine";
|
||||
}
|
||||
|
||||
const fs::path endpoint_base_dir = sockets.base_dir;
|
||||
const fs::path group_socket_path =
|
||||
generate_group_endpoint(group_name, wine_prefix, plugin_arch);
|
||||
try {
|
||||
@@ -175,9 +174,10 @@ GroupHost::GroupHost(
|
||||
boost::asio::local::stream_protocol::socket group_socket(io_context);
|
||||
group_socket.connect(group_socket_path.string());
|
||||
|
||||
write_object(group_socket,
|
||||
GroupRequest{.plugin_path = plugin_path.string(),
|
||||
.socket_path = socket_endpoint.string()});
|
||||
write_object(
|
||||
group_socket,
|
||||
GroupRequest{.plugin_path = plugin_path.string(),
|
||||
.endpoint_base_dir = endpoint_base_dir.string()});
|
||||
const auto response = read_object<GroupResponse>(group_socket);
|
||||
|
||||
host_pid = response.pid;
|
||||
@@ -199,7 +199,7 @@ GroupHost::GroupHost(
|
||||
// meantime.
|
||||
group_host_connect_handler = std::jthread([&, group_socket_path,
|
||||
plugin_path,
|
||||
socket_endpoint]() {
|
||||
endpoint_base_dir]() {
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
// TODO: Replace this polling with inotify
|
||||
@@ -214,8 +214,9 @@ GroupHost::GroupHost(
|
||||
|
||||
write_object(
|
||||
group_socket,
|
||||
GroupRequest{.plugin_path = plugin_path.string(),
|
||||
.socket_path = socket_endpoint.string()});
|
||||
GroupRequest{
|
||||
.plugin_path = plugin_path.string(),
|
||||
.endpoint_base_dir = endpoint_base_dir.string()});
|
||||
const auto response =
|
||||
read_object<GroupResponse>(group_socket);
|
||||
|
||||
@@ -262,7 +263,5 @@ void GroupHost::terminate() {
|
||||
// There's no need to manually terminate group host processes as they will
|
||||
// shut down automatically after all plugins have exited. Manually closing
|
||||
// the dispatch socket will cause the associated plugin to exit.
|
||||
host_vst_dispatch.shutdown(
|
||||
boost::asio::local::stream_protocol::socket::shutdown_both);
|
||||
host_vst_dispatch.close();
|
||||
sockets.host_vst_dispatch.close();
|
||||
}
|
||||
|
||||
+11
-13
@@ -25,6 +25,7 @@
|
||||
#include <boost/process/child.hpp>
|
||||
#include <thread>
|
||||
|
||||
#include "../common/communication.h"
|
||||
#include "../common/logging.h"
|
||||
#include "utils.h"
|
||||
|
||||
@@ -117,7 +118,7 @@ class IndividualHost : public HostProcess {
|
||||
* handled on.
|
||||
* @param logger The `Logger` instance the redirected STDIO streams will be
|
||||
* written to.
|
||||
* @param socket_endpoint The endpoint that should be used to communicate
|
||||
* @param sockets The socket endpoints that will be used for communication
|
||||
* with the plugin.
|
||||
*
|
||||
* @throw std::runtime_error When `plugin_path` does not point to a valid
|
||||
@@ -126,7 +127,7 @@ class IndividualHost : public HostProcess {
|
||||
IndividualHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
boost::filesystem::path plugin_path,
|
||||
boost::filesystem::path socket_endpoint);
|
||||
const Sockets<std::jthread>& sockets);
|
||||
|
||||
PluginArchitecture architecture() override;
|
||||
boost::filesystem::path path() override;
|
||||
@@ -160,19 +161,16 @@ class GroupHost : public HostProcess {
|
||||
* handled on.
|
||||
* @param logger The `Logger` instance the redirected STDIO streams will be
|
||||
* written to.
|
||||
* @param socket_endpoint The endpoint that should be used to communicate
|
||||
* with the plugin.
|
||||
* @param sockets The socket endpoints that will be used for communication
|
||||
* with the plugin. When the plugin shuts down, we'll terminate the
|
||||
* dispatch socket contained in this object.
|
||||
* @param group_name The name of the plugin group.
|
||||
* @param host_vst_dispatch The socket used to communicate
|
||||
* `AEffect::dispatcher()` events with this plugin. Will be closed as to
|
||||
* shut down the plugin.
|
||||
*/
|
||||
GroupHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
boost::filesystem::path plugin_path,
|
||||
boost::filesystem::path socket_endpoint,
|
||||
std::string group_name,
|
||||
boost::asio::local::stream_protocol::socket& host_vst_dispatch);
|
||||
Sockets<std::jthread>& socket_endpoint,
|
||||
std::string group_name);
|
||||
|
||||
PluginArchitecture architecture() override;
|
||||
boost::filesystem::path path() override;
|
||||
@@ -191,10 +189,10 @@ class GroupHost : public HostProcess {
|
||||
pid_t host_pid;
|
||||
|
||||
/**
|
||||
* The associated dispatch socket for the plugin we're hosting. This is used
|
||||
* to terminate the plugin.
|
||||
* The associated sockets for the plugin we're hosting. This is used to
|
||||
* terminate the plugin.
|
||||
*/
|
||||
boost::asio::local::stream_protocol::socket& host_vst_dispatch;
|
||||
Sockets<std::jthread>& sockets;
|
||||
|
||||
/**
|
||||
* A thread that waits for the group host to have started and then ask it to
|
||||
|
||||
+61
-117
@@ -21,7 +21,6 @@
|
||||
#include <src/common/config/version.h>
|
||||
|
||||
#include "../common/communication.h"
|
||||
#include "../common/events.h"
|
||||
#include "../common/utils.h"
|
||||
#include "utils.h"
|
||||
|
||||
@@ -54,32 +53,26 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
|
||||
// bridge will crash otherwise
|
||||
plugin(),
|
||||
io_context(),
|
||||
socket_endpoint(generate_plugin_endpoint().string()),
|
||||
socket_acceptor(io_context, socket_endpoint),
|
||||
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),
|
||||
sockets(io_context,
|
||||
generate_endpoint_base(
|
||||
vst_plugin_path.filename().replace_extension("").string()),
|
||||
true),
|
||||
host_callback_function(host_callback),
|
||||
logger(Logger::create_from_environment(
|
||||
create_logger_prefix(socket_endpoint.path()))),
|
||||
create_logger_prefix(sockets.base_dir))),
|
||||
wine_version(get_wine_version()),
|
||||
vst_host(
|
||||
config.group
|
||||
? std::unique_ptr<HostProcess>(
|
||||
std::make_unique<GroupHost>(io_context,
|
||||
logger,
|
||||
vst_plugin_path,
|
||||
socket_endpoint.path(),
|
||||
*config.group,
|
||||
host_vst_dispatch))
|
||||
: std::unique_ptr<HostProcess>(
|
||||
std::make_unique<IndividualHost>(io_context,
|
||||
vst_host(config.group
|
||||
? std::unique_ptr<HostProcess>(
|
||||
std::make_unique<GroupHost>(io_context,
|
||||
logger,
|
||||
vst_plugin_path,
|
||||
socket_endpoint.path()))),
|
||||
sockets,
|
||||
*config.group))
|
||||
: std::unique_ptr<HostProcess>(
|
||||
std::make_unique<IndividualHost>(io_context,
|
||||
logger,
|
||||
vst_plugin_path,
|
||||
sockets))),
|
||||
has_realtime_priority(set_realtime_priority()),
|
||||
wine_io_handler([&]() { io_context.run(); }) {
|
||||
log_init_message();
|
||||
@@ -107,24 +100,13 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
|
||||
});
|
||||
#endif
|
||||
|
||||
// It's very important that these sockets are connected to in the same
|
||||
// order in the Wine VST host
|
||||
socket_acceptor.accept(host_vst_dispatch);
|
||||
socket_acceptor.accept(host_vst_dispatch_midi_events);
|
||||
socket_acceptor.accept(vst_host_callback);
|
||||
socket_acceptor.accept(host_vst_parameters);
|
||||
socket_acceptor.accept(host_vst_process_replacing);
|
||||
socket_acceptor.accept(host_vst_control);
|
||||
|
||||
// This will block until all sockets have been connected to by the Wine VST
|
||||
// host
|
||||
sockets.connect();
|
||||
#ifndef WITH_WINEDBG
|
||||
host_guard_handler.request_stop();
|
||||
#endif
|
||||
|
||||
// There's no need to keep the socket endpoint file around after accepting
|
||||
// all the sockets, and RAII won't clean these files up for us
|
||||
socket_acceptor.close();
|
||||
fs::remove(socket_endpoint.path());
|
||||
|
||||
// Set up all pointers for our `AEffect` struct. We will fill this with data
|
||||
// from the VST plugin loaded in Wine at the end of this constructor.
|
||||
plugin.ptr3 = this;
|
||||
@@ -139,41 +121,30 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
|
||||
// instead of asynchronous IO since communication has to be handled in
|
||||
// lockstep anyway
|
||||
host_callback_handler = std::jthread([&]() {
|
||||
while (true) {
|
||||
try {
|
||||
// TODO: Think of a nicer way to structure this and the similar
|
||||
// handler in `Vst2Bridge::handle_dispatch_midi_events`
|
||||
receive_event(
|
||||
vst_host_callback, std::pair<Logger&, bool>(logger, false),
|
||||
[&](Event& event) {
|
||||
// MIDI events sent from the plugin back to the host are
|
||||
// a special case here. They have to sent during the
|
||||
// `processReplacing()` function or else the host will
|
||||
// ignore them. Because of this we'll temporarily save
|
||||
// any MIDI events we receive here, and then we'll
|
||||
// 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);
|
||||
sockets.vst_host_callback.receive(
|
||||
std::pair<Logger&, bool>(logger, false),
|
||||
[&](Event& event, bool /*on_main_thread*/) {
|
||||
// MIDI events sent from the plugin back to the host are a
|
||||
// special case here. They have to sent during the
|
||||
// `processReplacing()` function or else the host will ignore
|
||||
// them. Because of this we'll temporarily save any MIDI events
|
||||
// we receive here, and then we'll 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(
|
||||
std::get<DynamicVstEvents>(event.payload));
|
||||
EventResult response{.return_value = 1,
|
||||
.payload = nullptr,
|
||||
.value_payload = std::nullopt};
|
||||
incoming_midi_events.push_back(
|
||||
std::get<DynamicVstEvents>(event.payload));
|
||||
EventResult response{.return_value = 1,
|
||||
.payload = nullptr,
|
||||
.value_payload = std::nullopt};
|
||||
|
||||
return response;
|
||||
} else {
|
||||
return passthrough_event(
|
||||
&plugin, 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;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
} else {
|
||||
return passthrough_event(&plugin,
|
||||
host_callback_function)(event);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Read the plugin's information from the Wine process. This can only be
|
||||
@@ -181,13 +152,14 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
|
||||
// call these during its initialization. Any further updates will be sent
|
||||
// over the `dispatcher()` socket. This would happen whenever the plugin
|
||||
// calls `audioMasterIOChanged()` and after the host calls `effOpen()`.
|
||||
const auto initialization_data = read_object<EventResult>(host_vst_control);
|
||||
const auto initialization_data =
|
||||
read_object<EventResult>(sockets.host_vst_control);
|
||||
const auto initialized_plugin =
|
||||
std::get<AEffect>(initialization_data.payload);
|
||||
|
||||
// After receiving the `AEffect` values we'll want to send the configuration
|
||||
// back to complete the startup process
|
||||
write_object(host_vst_control, config);
|
||||
write_object(sockets.host_vst_control, config);
|
||||
|
||||
update_aeffect(plugin, initialized_plugin);
|
||||
}
|
||||
@@ -452,10 +424,9 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
|
||||
intptr_t return_value = 0;
|
||||
try {
|
||||
// TODO: Add some kind of timeout?
|
||||
return_value =
|
||||
send_event(host_vst_dispatch, dispatch_mutex, converter,
|
||||
std::pair<Logger&, bool>(logger, true), opcode,
|
||||
index, value, data, option);
|
||||
return_value = sockets.host_vst_dispatch.send(
|
||||
converter, std::pair<Logger&, bool>(logger, true), opcode,
|
||||
index, value, data, option);
|
||||
} catch (const boost::system::system_error& a) {
|
||||
// Thrown when the socket gets closed because the VST plugin
|
||||
// loaded into the Wine process crashed during shutdown
|
||||
@@ -478,10 +449,9 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
|
||||
// thread and socket to pass MIDI events. Otherwise plugins will
|
||||
// stop receiving MIDI data when they have an open dropdowns or
|
||||
// message box.
|
||||
return send_event(host_vst_dispatch_midi_events,
|
||||
dispatch_midi_events_mutex, converter,
|
||||
std::pair<Logger&, bool>(logger, true), opcode,
|
||||
index, value, data, option);
|
||||
return sockets.host_vst_dispatch_midi_events.send(
|
||||
converter, std::pair<Logger&, bool>(logger, true), opcode,
|
||||
index, value, data, option);
|
||||
break;
|
||||
case effCanDo: {
|
||||
const std::string query(static_cast<const char*>(data));
|
||||
@@ -503,29 +473,6 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
|
||||
logger.log(" when using REAPER.");
|
||||
logger.log("");
|
||||
|
||||
// Since the user is using REAPER, also show a reminder that the
|
||||
// REAPER workaround should be enabled when it is not yet
|
||||
// enabled since it may be easy to miss
|
||||
if (!config.hack_reaper_update_display) {
|
||||
logger.log(
|
||||
" With using REAPER you will have to enable the");
|
||||
logger.log(
|
||||
" 'hack_reaper_update_display' option to prevent");
|
||||
logger.log(
|
||||
" certain plugins from crashing. To do so, create a");
|
||||
logger.log(
|
||||
" new file named 'yabridge.toml' next to your");
|
||||
logger.log(" plugins with the following contents:");
|
||||
logger.log("");
|
||||
logger.log(
|
||||
" # "
|
||||
"https://github.com/robbert-vdh/"
|
||||
"yabridge#runtime-dependencies-and-known-issues");
|
||||
logger.log(" [\"*\"]");
|
||||
logger.log(" hack_reaper_update_display = true");
|
||||
logger.log("");
|
||||
}
|
||||
|
||||
logger.log_event_response(true, opcode, -1, nullptr,
|
||||
std::nullopt);
|
||||
return -1;
|
||||
@@ -538,9 +485,9 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
|
||||
// and loading plugin state it's much better to have bitsery or our
|
||||
// receiving function temporarily allocate a large enough buffer rather than
|
||||
// to have a bunch of allocated memory sitting around doing nothing.
|
||||
return send_event(host_vst_dispatch, dispatch_mutex, converter,
|
||||
std::pair<Logger&, bool>(logger, true), opcode, index,
|
||||
value, data, option);
|
||||
return sockets.host_vst_dispatch.send(
|
||||
converter, std::pair<Logger&, bool>(logger, true), opcode, index, value,
|
||||
data, option);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
@@ -555,11 +502,11 @@ void PluginBridge::do_process(T** inputs, T** outputs, int sample_frames) {
|
||||
}
|
||||
|
||||
const AudioBuffers request{input_buffers, sample_frames};
|
||||
write_object(host_vst_process_replacing, request, process_buffer);
|
||||
write_object(sockets.host_vst_process_replacing, request, process_buffer);
|
||||
|
||||
// Write the results back to the `outputs` arrays
|
||||
const auto response =
|
||||
read_object<AudioBuffers>(host_vst_process_replacing, process_buffer);
|
||||
const auto response = read_object<AudioBuffers>(
|
||||
sockets.host_vst_process_replacing, process_buffer);
|
||||
const auto& response_buffers =
|
||||
std::get<std::vector<std::vector<T>>>(response.buffers);
|
||||
|
||||
@@ -608,8 +555,8 @@ float PluginBridge::get_parameter(AEffect* /*plugin*/, int index) {
|
||||
// called at the same time since they share the same socket
|
||||
{
|
||||
std::lock_guard lock(parameters_mutex);
|
||||
write_object(host_vst_parameters, request);
|
||||
response = read_object<ParameterResult>(host_vst_parameters);
|
||||
write_object(sockets.host_vst_parameters, request);
|
||||
response = read_object<ParameterResult>(sockets.host_vst_parameters);
|
||||
}
|
||||
|
||||
logger.log_get_parameter_response(*response.value);
|
||||
@@ -625,9 +572,9 @@ void PluginBridge::set_parameter(AEffect* /*plugin*/, int index, float value) {
|
||||
|
||||
{
|
||||
std::lock_guard lock(parameters_mutex);
|
||||
write_object(host_vst_parameters, request);
|
||||
write_object(sockets.host_vst_parameters, request);
|
||||
|
||||
response = read_object<ParameterResult>(host_vst_parameters);
|
||||
response = read_object<ParameterResult>(sockets.host_vst_parameters);
|
||||
}
|
||||
|
||||
logger.log_set_parameter_response();
|
||||
@@ -647,7 +594,8 @@ void PluginBridge::log_init_message() {
|
||||
<< std::endl;
|
||||
init_msg << "realtime: '" << (has_realtime_priority ? "yes" : "no")
|
||||
<< "'" << std::endl;
|
||||
init_msg << "socket: '" << socket_endpoint.path() << "'" << std::endl;
|
||||
init_msg << "sockets: '" << sockets.base_dir.string() << "'"
|
||||
<< std::endl;
|
||||
init_msg << "wine prefix: '";
|
||||
|
||||
// If the Wine prefix is manually overridden, then this should be made
|
||||
@@ -689,10 +637,6 @@ void PluginBridge::log_init_message() {
|
||||
if (config.editor_double_embed) {
|
||||
other_options.push_back("editor: double embed");
|
||||
}
|
||||
if (config.hack_reaper_update_display) {
|
||||
other_options.push_back(
|
||||
"hack: REAPER audioMasterUpdateDisplay() workaround");
|
||||
}
|
||||
if (!other_options.empty()) {
|
||||
init_msg << join_quoted_strings(other_options) << std::endl;
|
||||
} else {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "../common/communication.h"
|
||||
#include "../common/configuration.h"
|
||||
#include "../common/logging.h"
|
||||
#include "host-process.h"
|
||||
@@ -123,45 +124,7 @@ class PluginBridge {
|
||||
void log_init_message();
|
||||
|
||||
boost::asio::io_context io_context;
|
||||
boost::asio::local::stream_protocol::endpoint socket_endpoint;
|
||||
boost::asio::local::stream_protocol::acceptor socket_acceptor;
|
||||
|
||||
// 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;
|
||||
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;
|
||||
Sockets<std::jthread> sockets;
|
||||
|
||||
/**
|
||||
* The thread that handles host callbacks.
|
||||
@@ -169,16 +132,10 @@ class PluginBridge {
|
||||
std::jthread host_callback_handler;
|
||||
|
||||
/**
|
||||
* A binary semaphore to prevent race conditions from the dispatch function
|
||||
* being called by two threads at once. See `send_event()` for more
|
||||
* information.
|
||||
*/
|
||||
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.
|
||||
* A mutex to prevent multiple simultaneous calls to `getParameter()` and
|
||||
* `setParameter()`. This likely won't happen, but better safe than sorry.
|
||||
* For `dispatch()` and `audioMaster()` there's some more complex logic for
|
||||
* this in `EventHandler`.
|
||||
*/
|
||||
std::mutex parameters_mutex;
|
||||
|
||||
|
||||
+13
-55
@@ -22,37 +22,27 @@
|
||||
#include <boost/process/search_path.hpp>
|
||||
#include <boost/process/system.hpp>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
|
||||
// Generated inside of the build directory
|
||||
#include <src/common/config/config.h>
|
||||
|
||||
#include "../common/configuration.h"
|
||||
#include "../common/utils.h"
|
||||
|
||||
namespace bp = boost::process;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
/**
|
||||
* Used for generating random identifiers.
|
||||
*/
|
||||
constexpr char alphanumeric_characters[] =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
std::string create_logger_prefix(const fs::path& socket_path) {
|
||||
// Use the socket filename as the logger prefix, but strip the `yabridge-`
|
||||
// part since that's redundant
|
||||
std::string socket_name =
|
||||
socket_path.filename().replace_extension().string();
|
||||
std::string create_logger_prefix(const fs::path& endpoint_base_dir) {
|
||||
// Use the name of the base directory used for our sockets as the logger
|
||||
// prefix, but strip the `yabridge-` part since that's redundant
|
||||
std::string endpoint_name = endpoint_base_dir.filename().string();
|
||||
|
||||
constexpr std::string_view socket_prefix("yabridge-");
|
||||
assert(socket_name.starts_with(socket_prefix));
|
||||
socket_name = socket_name.substr(socket_prefix.size());
|
||||
assert(endpoint_name.starts_with(socket_prefix));
|
||||
endpoint_name = endpoint_name.substr(socket_prefix.size());
|
||||
|
||||
std::ostringstream prefix;
|
||||
prefix << "[" << socket_name << "] ";
|
||||
|
||||
return prefix.str();
|
||||
return "[" + endpoint_name + "] ";
|
||||
}
|
||||
|
||||
std::optional<fs::path> find_wineprefix() {
|
||||
@@ -183,39 +173,7 @@ boost::filesystem::path generate_group_endpoint(
|
||||
}
|
||||
socket_name << ".sock";
|
||||
|
||||
return fs::temp_directory_path() / socket_name.str();
|
||||
}
|
||||
|
||||
fs::path generate_plugin_endpoint() {
|
||||
const auto plugin_name =
|
||||
find_vst_plugin().filename().replace_extension("").string();
|
||||
|
||||
std::random_device random_device;
|
||||
std::mt19937 rng(random_device());
|
||||
fs::path candidate_endpoint;
|
||||
do {
|
||||
std::string random_id;
|
||||
std::sample(
|
||||
alphanumeric_characters,
|
||||
alphanumeric_characters + strlen(alphanumeric_characters) - 1,
|
||||
std::back_inserter(random_id), 8, rng);
|
||||
|
||||
// We'll get rid of the file descriptors immediately after accepting the
|
||||
// sockets, so putting them inside of a subdirectory would only leave
|
||||
// behind an empty directory
|
||||
std::ostringstream socket_name;
|
||||
socket_name << "yabridge-" << plugin_name << "-" << random_id
|
||||
<< ".sock";
|
||||
|
||||
candidate_endpoint = fs::temp_directory_path() / socket_name.str();
|
||||
} while (fs::exists(candidate_endpoint));
|
||||
|
||||
// TODO: Should probably try creating the endpoint right here and catch any
|
||||
// exceptions since this could technically result in a race condition
|
||||
// when two instances of yabridge decide to use the same endpoint name
|
||||
// at the same time
|
||||
|
||||
return candidate_endpoint;
|
||||
return get_temporary_directory() / socket_name.str();
|
||||
}
|
||||
|
||||
fs::path get_this_file_location() {
|
||||
@@ -243,7 +201,7 @@ std::string get_wine_version() {
|
||||
|
||||
bp::environment env = boost::this_process::environment();
|
||||
if (!env["WINELOADER"].empty()) {
|
||||
wine_command = env.get("WINELOADER");
|
||||
wine_command = env["WINELOADER"].to_string();
|
||||
}
|
||||
|
||||
bp::ipstream output;
|
||||
@@ -271,13 +229,13 @@ std::string get_wine_version() {
|
||||
|
||||
std::string join_quoted_strings(std::vector<std::string>& strings) {
|
||||
bool is_first = true;
|
||||
std::ostringstream joined_strigns{};
|
||||
std::ostringstream joined_strings{};
|
||||
for (const auto& option : strings) {
|
||||
joined_strigns << (is_first ? "'" : ", '") << option << "'";
|
||||
joined_strings << (is_first ? "'" : ", '") << option << "'";
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
return joined_strigns.str();
|
||||
return joined_strings.str();
|
||||
}
|
||||
|
||||
Configuration load_config_for(const fs::path& yabridge_path) {
|
||||
|
||||
+12
-19
@@ -46,15 +46,17 @@ class patched_async_pipe : public boost::process::async_pipe {
|
||||
enum class PluginArchitecture { vst_32, vst_64 };
|
||||
|
||||
/**
|
||||
* Create a logger prefix based on the unique socket path for easy
|
||||
* identification. The socket path contains both the plugin's name and a unique
|
||||
* identifier.
|
||||
* Create a logger prefix based on the endpoint base directory used for the
|
||||
* sockets for easy identification. This will result in a prefix of the form
|
||||
* `[<plugin_name>-<random_id>] `.
|
||||
*
|
||||
* @param socket_path The path to the socket endpoint in use.
|
||||
* @param endpoint_base_dir A directory name generated by
|
||||
* `generate_endpoint_base()`.
|
||||
*
|
||||
* @return A prefix string for log messages.
|
||||
*/
|
||||
std::string create_logger_prefix(const boost::filesystem::path& socket_path);
|
||||
std::string create_logger_prefix(
|
||||
const boost::filesystem::path& endpoint_base_dir);
|
||||
|
||||
/**
|
||||
* Determine the architecture of a VST plugin (or rather, a .dll file) based on
|
||||
@@ -117,10 +119,11 @@ std::optional<boost::filesystem::path> find_wineprefix();
|
||||
/**
|
||||
* Generate the group socket endpoint name used based on the name of the group,
|
||||
* the Wine prefix in use and the plugin architecture. The resulting format is
|
||||
* `/tmp/yabridge-group-<group_name>-<wine_prefix_id>-<architecture>.sock`. In
|
||||
* this socket name the `wine_prefix_id` is a numerical hash based on the Wine
|
||||
* prefix in use. This way the same group name can be used for multiple Wine
|
||||
* prefixes and for both 32 and 64 bit plugins without clashes.
|
||||
* in the form
|
||||
* `/run/user/<uid>/yabridge-group-<group_name>-<wine_prefix_id>-<architecture>.sock`.
|
||||
* In this socket name the `wine_prefix_id` is a numerical hash based on the
|
||||
* Wine prefix in use. This way the same group name can be used for multiple
|
||||
* Wine prefixes and for both 32 and 64 bit plugins without clashes.
|
||||
*
|
||||
* @param group_name The name of the plugin group.
|
||||
* @param wine_prefix The name of the Wine prefix in use. This should be
|
||||
@@ -140,16 +143,6 @@ boost::filesystem::path generate_group_endpoint(
|
||||
const boost::filesystem::path& wine_prefix,
|
||||
const PluginArchitecture architecture);
|
||||
|
||||
/**
|
||||
* Generate a unique name for the Unix domain socket endpoint based on the VST
|
||||
* plugin's name. This will also generate the parent directory if it does not
|
||||
* yet exist since we're using this in the constructor's initializer list.
|
||||
*
|
||||
* @return A path to a not yet existing Unix domain socket endpoint.
|
||||
* @throw std::runtime_error If no matching .dll file could be found.
|
||||
*/
|
||||
boost::filesystem::path generate_plugin_endpoint();
|
||||
|
||||
/**
|
||||
* Return a path to this `.so` file. This can be used to find out from where
|
||||
* this link to or copy of `libyabridge.so` was loaded.
|
||||
|
||||
@@ -29,11 +29,6 @@ namespace fs = boost::filesystem;
|
||||
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
/**
|
||||
* The delay between calls to the event loop at a more than cinematic 30 fps.
|
||||
*/
|
||||
constexpr std::chrono::duration event_loop_interval = 1000ms / 30;
|
||||
|
||||
/**
|
||||
* Listen on the specified endpoint if no process is already listening there,
|
||||
* otherwise throw. This is needed to handle these three situations:
|
||||
@@ -89,37 +84,40 @@ StdIoCapture::~StdIoCapture() {
|
||||
GroupBridge::GroupBridge(boost::filesystem::path group_socket_path)
|
||||
: logger(Logger::create_from_environment(
|
||||
create_logger_prefix(group_socket_path))),
|
||||
plugin_context(),
|
||||
main_context(),
|
||||
stdio_context(),
|
||||
stdout_redirect(stdio_context, STDOUT_FILENO),
|
||||
stderr_redirect(stdio_context, STDERR_FILENO),
|
||||
group_socket_endpoint(group_socket_path.string()),
|
||||
group_socket_acceptor(
|
||||
create_acceptor_if_inactive(plugin_context, group_socket_endpoint)),
|
||||
events_timer(plugin_context),
|
||||
shutdown_timer(plugin_context) {
|
||||
group_socket_acceptor(create_acceptor_if_inactive(main_context.context,
|
||||
group_socket_endpoint)),
|
||||
shutdown_timer(main_context.context) {
|
||||
// Write this process's original STDOUT and STDERR streams to the logger
|
||||
// TODO: This works for output generated by plugins, but not for debug
|
||||
// messages generated by wineserver. Is it possible to catch those?
|
||||
async_log_pipe_lines(stdout_redirect.pipe, stdout_buffer, "[STDOUT] ");
|
||||
async_log_pipe_lines(stderr_redirect.pipe, stderr_buffer, "[STDERR] ");
|
||||
|
||||
stdio_handler = std::jthread([&]() { stdio_context.run(); });
|
||||
stdio_handler = Win32Thread([&]() { stdio_context.run(); });
|
||||
}
|
||||
|
||||
GroupBridge::~GroupBridge() {
|
||||
stdio_context.stop();
|
||||
}
|
||||
|
||||
void GroupBridge::handle_plugin_dispatch(const GroupRequest request) {
|
||||
void GroupBridge::handle_plugin_dispatch(size_t plugin_id) {
|
||||
// At this point the `active_plugins` map will already contain the
|
||||
// intialized plugin's `Vst2Bridge` instance and this thread's handle
|
||||
auto& [thread, bridge] = active_plugins.at(request);
|
||||
Vst2Bridge* bridge;
|
||||
{
|
||||
std::lock_guard lock(active_plugins_mutex);
|
||||
bridge = active_plugins[plugin_id].second.get();
|
||||
}
|
||||
|
||||
// Blocks this thread until the plugin shuts down, handling all events on
|
||||
// the main IO context
|
||||
bridge->handle_dispatch();
|
||||
logger.log("'" + request.plugin_path + "' has exited");
|
||||
logger.log("'" + bridge->vst_plugin_path.string() + "' has exited");
|
||||
|
||||
// After the plugin has exited we'll remove this thread's plugin from the
|
||||
// active plugins. This is done within the IO context because the call to
|
||||
@@ -127,11 +125,12 @@ void GroupBridge::handle_plugin_dispatch(const GroupRequest request) {
|
||||
// potentially corrupt our heap. This way we can also properly join the
|
||||
// thread again. If no active plugins remain, then we'll terminate the
|
||||
// process.
|
||||
boost::asio::post(plugin_context, [&, request]() {
|
||||
boost::asio::post(main_context.context, [this, plugin_id]() {
|
||||
std::lock_guard lock(active_plugins_mutex);
|
||||
|
||||
// The join is implicit because we're using std::jthread
|
||||
active_plugins.erase(request);
|
||||
// The join is implicit because we're using Win32Thread (which mimics
|
||||
// std::jthread)
|
||||
active_plugins.erase(plugin_id);
|
||||
});
|
||||
|
||||
// Defer actually shutting down the process to allow for fast plugin
|
||||
@@ -148,7 +147,7 @@ void GroupBridge::handle_plugin_dispatch(const GroupRequest request) {
|
||||
if (active_plugins.size() == 0) {
|
||||
logger.log(
|
||||
"All plugins have exited, shutting down the group process");
|
||||
plugin_context.stop();
|
||||
main_context.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -159,7 +158,7 @@ void GroupBridge::handle_incoming_connections() {
|
||||
|
||||
logger.log(
|
||||
"Group host is up and running, now accepting incoming connections");
|
||||
plugin_context.run();
|
||||
main_context.run();
|
||||
}
|
||||
|
||||
bool GroupBridge::should_skip_message_loop() {
|
||||
@@ -189,7 +188,7 @@ void GroupBridge::accept_requests() {
|
||||
logger.log("Error while listening for incoming connections:");
|
||||
logger.log(error.message());
|
||||
|
||||
plugin_context.stop();
|
||||
main_context.stop();
|
||||
}
|
||||
|
||||
// Read the parameters, and then host the plugin in this process
|
||||
@@ -201,27 +200,28 @@ void GroupBridge::accept_requests() {
|
||||
const auto request = read_object<GroupRequest>(socket);
|
||||
write_object(socket, GroupResponse{boost::this_process::get_id()});
|
||||
|
||||
// Collisions in the generated socket names should be very rare, but
|
||||
// it could in theory happen
|
||||
assert(!active_plugins.contains(request));
|
||||
|
||||
// The plugin has to be initiated on the IO context's thread because
|
||||
// this has to be done on the same thread that's handling messages,
|
||||
// and all window messages have to be handled from the same thread.
|
||||
logger.log("Received request to host '" + request.plugin_path +
|
||||
"' using socket '" + request.socket_path + "'");
|
||||
"' using socket endpoint base directory '" +
|
||||
request.endpoint_base_dir + "'");
|
||||
try {
|
||||
auto bridge = std::make_unique<Vst2Bridge>(
|
||||
plugin_context, request.plugin_path, request.socket_path);
|
||||
main_context, request.plugin_path,
|
||||
request.endpoint_base_dir);
|
||||
logger.log("Finished initializing '" + request.plugin_path +
|
||||
"'");
|
||||
|
||||
// Start listening for dispatcher events sent to the plugin's
|
||||
// socket on another thread. The actual event handling will
|
||||
// still occur within this IO context.
|
||||
active_plugins[request] =
|
||||
std::pair(std::jthread([&, request]() {
|
||||
handle_plugin_dispatch(request);
|
||||
// still be posted to this IO context so that every plugin's
|
||||
// primary event handling happens on the main thread. Since this
|
||||
// is only used within this context we don't need any locks.
|
||||
const size_t plugin_id = next_plugin_id.fetch_add(1);
|
||||
active_plugins[plugin_id] =
|
||||
std::pair(Win32Thread([this, plugin_id]() {
|
||||
handle_plugin_dispatch(plugin_id);
|
||||
}),
|
||||
std::move(bridge));
|
||||
} catch (const std::runtime_error& error) {
|
||||
@@ -235,16 +235,7 @@ void GroupBridge::accept_requests() {
|
||||
}
|
||||
|
||||
void GroupBridge::async_handle_events() {
|
||||
// Try to keep a steady framerate, but add in delays to let other events get
|
||||
// handled if the GUI message handling somehow takes very long.
|
||||
events_timer.expires_at(
|
||||
std::max(events_timer.expiry() + event_loop_interval,
|
||||
std::chrono::steady_clock::now() + 5ms));
|
||||
events_timer.async_wait([&](const boost::system::error_code& error) {
|
||||
if (error.failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
main_context.async_handle_events([&]() {
|
||||
{
|
||||
// Always handle X11 events
|
||||
std::lock_guard lock(active_plugins_mutex);
|
||||
@@ -274,8 +265,6 @@ void GroupBridge::async_handle_events() {
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
async_handle_events();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <boost/asio/streambuf.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
#include "vst2.h"
|
||||
@@ -132,21 +133,21 @@ class GroupBridge {
|
||||
* `accept_requests` since it has to be initiated inside of the IO context's
|
||||
* thread.
|
||||
*
|
||||
* Once the plugin has exited, this thread will then remove itself from the
|
||||
* `active_plugins` map. If this causes the vector to become empty, we will
|
||||
* terminate this process. This check will be delayed by a few seconds to
|
||||
* prevent having to constantly restart the group process during plugin
|
||||
* scanning.
|
||||
* Once the plugin has exited, this thread will then be joined to the main
|
||||
* thread and removed from the `active_plugins` from the main IO context. If
|
||||
* this causes the vector to become empty, we will terminate this process.
|
||||
* This check is delayed by a few seconds to prevent having to constantly
|
||||
* restart the group process during plugin scanning.
|
||||
*
|
||||
* @param request Information about the plugin to launch, i.e. the path to
|
||||
* the plugin and the path of the socket endpoint that will be used for
|
||||
* communication.
|
||||
* @param plugin_id The ID of this plugin in the `active_plugins` map. The
|
||||
* thread can fetch the plugin's `Vst2Bridge` instance from that map using
|
||||
* this identifier.
|
||||
*
|
||||
* @note In the case that the process starts but no plugin gets initiated,
|
||||
* then the process will never exit on its own. This should not happen
|
||||
* though.
|
||||
*/
|
||||
void handle_plugin_dispatch(const GroupRequest request);
|
||||
void handle_plugin_dispatch(size_t plugin_id);
|
||||
|
||||
/**
|
||||
* Listen for new requests to spawn plugins within this process and handle
|
||||
@@ -178,7 +179,7 @@ class GroupBridge {
|
||||
* event handling and message loop interaction also has to be done from that
|
||||
* thread, which is why we initialize the plugin here and use the
|
||||
* `handle_dispatch()` function to run events within the same
|
||||
* `plugin_context`.
|
||||
* `main_context`.
|
||||
*
|
||||
* @see handle_plugin_dispatch
|
||||
*/
|
||||
@@ -216,13 +217,13 @@ class GroupBridge {
|
||||
* operations that may involve the Win32 mesasge loop (e.g. initialization
|
||||
* and most `AEffect::dispatcher()` calls) should be run on.
|
||||
*/
|
||||
boost::asio::io_context plugin_context;
|
||||
MainContext main_context;
|
||||
/**
|
||||
* A seperate IO context that handles the STDIO redirect through
|
||||
* `StdIoCapture`. This is seperated the `plugin_context` above so that
|
||||
* STDIO capture does not get blocked by blocking GUI operations. Since
|
||||
* every GUI related operation should be run from the same thread, we can't
|
||||
* just add another thread to the main IO context.
|
||||
* `StdIoCapture`. This is separated from the `main_context` above so that
|
||||
* STDIO capture does not get blocked by GUI operations. Since every GUI
|
||||
* related operation should be run from the same thread, we can't just add
|
||||
* another thread to the main IO context.
|
||||
*/
|
||||
boost::asio::io_context stdio_context;
|
||||
|
||||
@@ -243,7 +244,7 @@ class GroupBridge {
|
||||
/**
|
||||
* A thread that runs the `stdio_context` loop.
|
||||
*/
|
||||
std::jthread stdio_handler;
|
||||
Win32Thread stdio_handler;
|
||||
|
||||
boost::asio::local::stream_protocol::endpoint group_socket_endpoint;
|
||||
/**
|
||||
@@ -257,11 +258,19 @@ class GroupBridge {
|
||||
* along with their plugin instance. After a plugin has exited or its
|
||||
* initialization has failed, the thread handling it will remove itself from
|
||||
* this map. This is to keep track of the amount of plugins currently
|
||||
* running with their associated thread handles.
|
||||
* running with their associated thread handles. The key that identifies the
|
||||
* thread and plugin is a unique plugin ID obtained by doing a fetch-and-add
|
||||
* on `next_plugin_id`.
|
||||
*/
|
||||
std::unordered_map<GroupRequest,
|
||||
std::pair<std::jthread, std::unique_ptr<Vst2Bridge>>>
|
||||
std::unordered_map<size_t,
|
||||
std::pair<Win32Thread, std::unique_ptr<Vst2Bridge>>>
|
||||
active_plugins;
|
||||
/**
|
||||
* A counter for the next unique plugin ID. When hosting a new plugin we'll
|
||||
* do a fetch-and-add to ensure that every thread gets its own unique
|
||||
* identifier.
|
||||
*/
|
||||
std::atomic_size_t next_plugin_id;
|
||||
/**
|
||||
* A mutex to prevent two threads from simultaneously accessing the plugins
|
||||
* map, and also to prevent `handle_plugin_dispatch()` from terminating the
|
||||
@@ -270,14 +279,6 @@ class GroupBridge {
|
||||
*/
|
||||
std::mutex active_plugins_mutex;
|
||||
|
||||
/**
|
||||
* A timer used to repeatedly handle the Win32 message loop and the X11
|
||||
* events.
|
||||
*
|
||||
8 @see async_handle_events
|
||||
*/
|
||||
boost::asio::steady_timer events_timer;
|
||||
|
||||
/**
|
||||
* A timer to defer shutting down the process, allowing for fast plugin
|
||||
* scanning without having to start a new group host process for each
|
||||
|
||||
+235
-281
@@ -19,9 +19,9 @@
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
|
||||
#include "../../common/communication.h"
|
||||
#include "../../common/events.h"
|
||||
|
||||
/**
|
||||
* A function pointer to what should be the entry point of a VST plugin.
|
||||
@@ -39,16 +39,17 @@ Vst2Bridge* current_bridge_instance = nullptr;
|
||||
*/
|
||||
std::mutex current_bridge_instance_mutex;
|
||||
|
||||
/**
|
||||
* Opcodes that should always be handled on the main thread because they may
|
||||
* involve GUI operations.
|
||||
*/
|
||||
const std::set<int> unsafe_opcodes{effOpen, effClose, effEditGetRect,
|
||||
effEditOpen, effEditClose, effEditIdle,
|
||||
effEditTop};
|
||||
|
||||
intptr_t VST_CALL_CONV
|
||||
host_callback_proxy(AEffect*, int, int, intptr_t, void*, float);
|
||||
|
||||
// We need to use the `CreateThread` WinAPI functions instead of `std::thread`
|
||||
// to use the correct calling conventions within threads. Otherwise we'll get
|
||||
// some rare and impossible to debug data races in some particular plugins.
|
||||
uint32_t WINAPI handle_dispatch_midi_events_proxy(void*);
|
||||
uint32_t WINAPI handle_parameters_proxy(void*);
|
||||
uint32_t WINAPI handle_process_replacing_proxy(void*);
|
||||
|
||||
/**
|
||||
* Fetch the Vst2Bridge instance stored in one of the two pointers reserved
|
||||
* for the host of the hosted VST plugin. This is sadly needed as a workaround
|
||||
@@ -65,18 +66,13 @@ Vst2Bridge& get_bridge_instance(const AEffect* plugin) {
|
||||
return *static_cast<Vst2Bridge*>(plugin->ptr1);
|
||||
}
|
||||
|
||||
Vst2Bridge::Vst2Bridge(boost::asio::io_context& main_context,
|
||||
Vst2Bridge::Vst2Bridge(MainContext& main_context,
|
||||
std::string plugin_dll_path,
|
||||
std::string socket_endpoint_path)
|
||||
: io_context(main_context),
|
||||
std::string endpoint_base_dir)
|
||||
: vst_plugin_path(plugin_dll_path),
|
||||
main_context(main_context),
|
||||
plugin_handle(LoadLibrary(plugin_dll_path.c_str()), FreeLibrary),
|
||||
socket_endpoint(socket_endpoint_path),
|
||||
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) {
|
||||
sockets(main_context.context, endpoint_base_dir, false) {
|
||||
// Got to love these C APIs
|
||||
if (!plugin_handle) {
|
||||
throw std::runtime_error("Could not load the Windows .dll file at '" +
|
||||
@@ -101,14 +97,7 @@ Vst2Bridge::Vst2Bridge(boost::asio::io_context& main_context,
|
||||
"'.");
|
||||
}
|
||||
|
||||
// It's very important that these sockets are accepted to in the same order
|
||||
// in the Linux plugin
|
||||
host_vst_dispatch.connect(socket_endpoint);
|
||||
host_vst_dispatch_midi_events.connect(socket_endpoint);
|
||||
vst_host_callback.connect(socket_endpoint);
|
||||
host_vst_parameters.connect(socket_endpoint);
|
||||
host_vst_process_replacing.connect(socket_endpoint);
|
||||
host_vst_control.connect(socket_endpoint);
|
||||
sockets.connect();
|
||||
|
||||
// Initialize after communication has been set up
|
||||
// We'll try to do the same `get_bridge_isntance` trick as in
|
||||
@@ -132,24 +121,212 @@ Vst2Bridge::Vst2Bridge(boost::asio::io_context& main_context,
|
||||
// of this object will be sent over the `dispatcher()` socket. This would be
|
||||
// done after the host calls `effOpen()`, and when the plugin calls
|
||||
// `audioMasterIOChanged()`.
|
||||
write_object(host_vst_control, EventResult{.return_value = 0,
|
||||
.payload = *plugin,
|
||||
.value_payload = std::nullopt});
|
||||
write_object(sockets.host_vst_control,
|
||||
EventResult{.return_value = 0,
|
||||
.payload = *plugin,
|
||||
.value_payload = std::nullopt});
|
||||
|
||||
// After sending the AEffect struct we'll receive this instance's
|
||||
// configuration as a response
|
||||
config = read_object<Configuration>(host_vst_control);
|
||||
config = read_object<Configuration>(sockets.host_vst_control);
|
||||
|
||||
// This works functionally identically to the `handle_dispatch()` function,
|
||||
// but this socket will only handle MIDI events and it will handle them
|
||||
// eagerly. This is needed because of Win32 API limitations.
|
||||
dispatch_midi_events_handler =
|
||||
Win32Thread(handle_dispatch_midi_events_proxy, this);
|
||||
dispatch_midi_events_handler = Win32Thread([&]() {
|
||||
sockets.host_vst_dispatch_midi_events.receive(
|
||||
std::nullopt, [&](Event& event, bool /*on_main_thread*/) {
|
||||
if (BOOST_LIKELY(event.opcode == effProcessEvents)) {
|
||||
// For 99% of the plugins we can just call
|
||||
// `effProcessReplacing()` and be done with it, but a select
|
||||
// few plugins (I could only find Kontakt that does this)
|
||||
// don't actually make copies of the events they receive and
|
||||
// only store pointers, meaning that they have to live at
|
||||
// least until the next audio buffer gets processed. We're
|
||||
// not using `passthrough_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);
|
||||
|
||||
parameters_handler = Win32Thread(handle_parameters_proxy, this);
|
||||
next_audio_buffer_midi_events.push_back(
|
||||
std::get<DynamicVstEvents>(event.payload));
|
||||
DynamicVstEvents& events =
|
||||
next_audio_buffer_midi_events.back();
|
||||
|
||||
process_replacing_handler =
|
||||
Win32Thread(handle_process_replacing_proxy, this);
|
||||
// Exact same handling as in `passthrough_event`, apart from
|
||||
// making a copy of the events first
|
||||
const intptr_t return_value = plugin->dispatcher(
|
||||
plugin, event.opcode, event.index, event.value,
|
||||
&events.as_c_events(), event.option);
|
||||
|
||||
EventResult response{.return_value = return_value,
|
||||
.payload = nullptr,
|
||||
.value_payload = std::nullopt};
|
||||
|
||||
return response;
|
||||
} else {
|
||||
using namespace std::placeholders;
|
||||
|
||||
std::cerr << "[Warning] Received non-MIDI "
|
||||
"event on MIDI processing thread"
|
||||
<< std::endl;
|
||||
|
||||
// Maybe this should just be a hard error instead, since it
|
||||
// should never happen
|
||||
return passthrough_event(
|
||||
plugin, std::bind(&Vst2Bridge::dispatch_wrapper, this,
|
||||
_1, _2, _3, _4, _5, _6))(event);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
parameters_handler = Win32Thread([&]() {
|
||||
while (true) {
|
||||
try {
|
||||
// Both `getParameter` and `setParameter` functions are passed
|
||||
// through on this socket since they have a lot of overlap. The
|
||||
// presence of the `value` field tells us which one we're
|
||||
// dealing with.
|
||||
auto request =
|
||||
read_object<Parameter>(sockets.host_vst_parameters);
|
||||
if (request.value) {
|
||||
// `setParameter`
|
||||
plugin->setParameter(plugin, request.index, *request.value);
|
||||
|
||||
ParameterResult response{std::nullopt};
|
||||
write_object(sockets.host_vst_parameters, response);
|
||||
} else {
|
||||
// `getParameter`
|
||||
float value = plugin->getParameter(plugin, request.index);
|
||||
|
||||
ParameterResult response{value};
|
||||
write_object(sockets.host_vst_parameters, response);
|
||||
}
|
||||
} catch (const boost::system::system_error&) {
|
||||
// The plugin has cut off communications, so we can shut down
|
||||
// this host application
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
process_replacing_handler = Win32Thread([&]() {
|
||||
// These are used as scratch buffers to prevent unnecessary allocations.
|
||||
// Since don't know in advance whether the host will call
|
||||
// `processReplacing` or `processDoubleReplacing` we'll just create
|
||||
// both.
|
||||
std::vector<std::vector<float>> output_buffers_single_precision(
|
||||
plugin->numOutputs);
|
||||
std::vector<std::vector<double>> output_buffers_double_precision(
|
||||
plugin->numOutputs);
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
auto request = read_object<AudioBuffers>(
|
||||
sockets.host_vst_process_replacing, process_buffer);
|
||||
// Let the plugin process the MIDI events that were received
|
||||
// since the last buffer, and then clean up those events. This
|
||||
// approach should not be needed but Kontakt only stores
|
||||
// pointers to rather than copies of the events.
|
||||
std::lock_guard lock(next_buffer_midi_events_mutex);
|
||||
|
||||
// Since the host should only be calling one of `process()`,
|
||||
// processReplacing()` or `processDoubleReplacing()`, we can all
|
||||
// handle them over the same socket. We pick which one to call
|
||||
// depending on the type of data we got sent and the plugin's
|
||||
// reported support for these functions.
|
||||
std::visit(
|
||||
overload{
|
||||
[&](std::vector<std::vector<float>>& input_buffers) {
|
||||
// The process functions expect a `float**` for
|
||||
// their inputs and their outputs
|
||||
std::vector<float*> inputs;
|
||||
for (auto& buffer : input_buffers) {
|
||||
inputs.push_back(buffer.data());
|
||||
}
|
||||
|
||||
// We reuse the buffers to avoid some unnecessary
|
||||
// heap allocations, so we need to make sure the
|
||||
// buffers are large enough since plugins can change
|
||||
// their output configuration. The type we're using
|
||||
// here (single precision floats vs double
|
||||
// precisioon doubles) should be the same as the one
|
||||
// we're sending in our response.
|
||||
std::vector<float*> outputs;
|
||||
output_buffers_single_precision.resize(
|
||||
plugin->numOutputs);
|
||||
for (auto& buffer :
|
||||
output_buffers_single_precision) {
|
||||
buffer.resize(request.sample_frames);
|
||||
outputs.push_back(buffer.data());
|
||||
}
|
||||
|
||||
// Any plugin made in the last fifteen years or so
|
||||
// should support `processReplacing`. In the off
|
||||
// chance it does not we can just emulate this
|
||||
// behavior ourselves.
|
||||
if (plugin->processReplacing) {
|
||||
plugin->processReplacing(plugin, inputs.data(),
|
||||
outputs.data(),
|
||||
request.sample_frames);
|
||||
} else {
|
||||
// If we zero out this buffer then the behavior
|
||||
// is the same as `processReplacing``
|
||||
for (std::vector<float>& buffer :
|
||||
output_buffers_single_precision) {
|
||||
std::fill(buffer.begin(), buffer.end(),
|
||||
0.0);
|
||||
}
|
||||
|
||||
plugin->process(plugin, inputs.data(),
|
||||
outputs.data(),
|
||||
request.sample_frames);
|
||||
}
|
||||
|
||||
AudioBuffers response{
|
||||
output_buffers_single_precision,
|
||||
request.sample_frames};
|
||||
write_object(sockets.host_vst_process_replacing,
|
||||
response, process_buffer);
|
||||
},
|
||||
[&](std::vector<std::vector<double>>& input_buffers) {
|
||||
// Exactly the same as the above, but for double
|
||||
// precision audio
|
||||
std::vector<double*> inputs;
|
||||
for (auto& buffer : input_buffers) {
|
||||
inputs.push_back(buffer.data());
|
||||
}
|
||||
|
||||
std::vector<double*> outputs;
|
||||
output_buffers_double_precision.resize(
|
||||
plugin->numOutputs);
|
||||
for (auto& buffer :
|
||||
output_buffers_double_precision) {
|
||||
buffer.resize(request.sample_frames);
|
||||
outputs.push_back(buffer.data());
|
||||
}
|
||||
|
||||
plugin->processDoubleReplacing(
|
||||
plugin, inputs.data(), outputs.data(),
|
||||
request.sample_frames);
|
||||
|
||||
AudioBuffers response{
|
||||
output_buffers_double_precision,
|
||||
request.sample_frames};
|
||||
write_object(sockets.host_vst_process_replacing,
|
||||
response, process_buffer);
|
||||
}},
|
||||
request.buffers);
|
||||
|
||||
next_audio_buffer_midi_events.clear();
|
||||
} catch (const boost::system::system_error&) {
|
||||
// The plugin has cut off communications, so we can shut down
|
||||
// this host application
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool Vst2Bridge::should_skip_message_loop() const {
|
||||
@@ -157,234 +334,36 @@ bool Vst2Bridge::should_skip_message_loop() const {
|
||||
}
|
||||
|
||||
void Vst2Bridge::handle_dispatch() {
|
||||
while (true) {
|
||||
try {
|
||||
receive_event(
|
||||
host_vst_dispatch, std::nullopt,
|
||||
passthrough_event(
|
||||
plugin,
|
||||
[&](AEffect* plugin, int opcode, int index, intptr_t value,
|
||||
void* data, float option) -> intptr_t {
|
||||
// Instead of running `plugin->dispatcher()` (or
|
||||
// `dispatch_wrapper()`) directly, we'll run the
|
||||
// function within the IO context so all events will be
|
||||
// executed on the same thread as the one that runs the
|
||||
// Win32 message loop
|
||||
sockets.host_vst_dispatch.receive(
|
||||
std::nullopt, [&](Event& event, bool /*on_main_thread*/) {
|
||||
// TODO: As per the TODO in `passthrough_event`, this can use a
|
||||
// round of refactoring now that we never use its returned
|
||||
// lambda directly anymore
|
||||
return passthrough_event(
|
||||
plugin,
|
||||
[&](AEffect* plugin, int opcode, int index, intptr_t value,
|
||||
void* data, float option) -> intptr_t {
|
||||
// Certain functions will most definitely involve the GUI or
|
||||
// the Win32 message loop. These functions have to be
|
||||
// performed on the thread that is running the IO context,
|
||||
// since this is also where the plugins were instantiated
|
||||
// and where the Win32 message loop is handled.
|
||||
if (unsafe_opcodes.contains(opcode)) {
|
||||
std::promise<intptr_t> dispatch_result;
|
||||
boost::asio::dispatch(io_context, [&]() {
|
||||
boost::asio::dispatch(main_context.context, [&]() {
|
||||
const intptr_t result = dispatch_wrapper(
|
||||
plugin, opcode, index, value, data, option);
|
||||
|
||||
dispatch_result.set_value(result);
|
||||
});
|
||||
|
||||
// The message loop and X11 event handling will be run
|
||||
// separately on a timer
|
||||
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() {
|
||||
while (true) {
|
||||
try {
|
||||
receive_event(
|
||||
host_vst_dispatch_midi_events, std::nullopt, [&](Event& event) {
|
||||
if (BOOST_LIKELY(event.opcode == effProcessEvents)) {
|
||||
// For 99% of the plugins we can just call
|
||||
// `effProcessReplacing()` and be done with it, but a
|
||||
// select few plugins (I could only find Kontakt that
|
||||
// does this) don't actually make copies of the events
|
||||
// they receive and only store pointers, meaning that
|
||||
// they have to live at least until the next audio
|
||||
// buffer gets processed. We're not using
|
||||
// `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(
|
||||
std::get<DynamicVstEvents>(event.payload));
|
||||
DynamicVstEvents& events =
|
||||
next_audio_buffer_midi_events.back();
|
||||
|
||||
// Exact same handling as in `passthrough_event`, apart
|
||||
// from making a copy of the events first
|
||||
const intptr_t return_value = plugin->dispatcher(
|
||||
plugin, event.opcode, event.index, event.value,
|
||||
&events.as_c_events(), event.option);
|
||||
|
||||
EventResult response{.return_value = return_value,
|
||||
.payload = nullptr,
|
||||
.value_payload = std::nullopt};
|
||||
|
||||
return response;
|
||||
} else {
|
||||
using namespace std::placeholders;
|
||||
|
||||
std::cerr << "[Warning] Received non-MIDI "
|
||||
"event on MIDI processing thread"
|
||||
<< std::endl;
|
||||
|
||||
// Maybe this should just be a hard error instead, since
|
||||
// it should never happen
|
||||
return passthrough_event(
|
||||
plugin,
|
||||
std::bind(&Vst2Bridge::dispatch_wrapper, this, _1,
|
||||
_2, _3, _4, _5, _6))(event);
|
||||
return dispatch_wrapper(plugin, opcode, index, value,
|
||||
data, option);
|
||||
}
|
||||
});
|
||||
} 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() {
|
||||
while (true) {
|
||||
try {
|
||||
// Both `getParameter` and `setParameter` functions are passed
|
||||
// through on this socket since they have a lot of overlap. The
|
||||
// presence of the `value` field tells us which one we're dealing
|
||||
// with.
|
||||
auto request = read_object<Parameter>(host_vst_parameters);
|
||||
if (request.value) {
|
||||
// `setParameter`
|
||||
plugin->setParameter(plugin, request.index, *request.value);
|
||||
|
||||
ParameterResult response{std::nullopt};
|
||||
write_object(host_vst_parameters, response);
|
||||
} else {
|
||||
// `getParameter`
|
||||
float value = plugin->getParameter(plugin, request.index);
|
||||
|
||||
ParameterResult response{value};
|
||||
write_object(host_vst_parameters, response);
|
||||
}
|
||||
} catch (const boost::system::system_error&) {
|
||||
// The plugin has cut off communications, so we can shut down this
|
||||
// host application
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Vst2Bridge::handle_process_replacing() {
|
||||
// These are used as scratch buffers to prevent unnecessary allocations.
|
||||
// Since don't know in advance whether the host will call `processReplacing`
|
||||
// or `processDoubleReplacing` we'll just create both.
|
||||
std::vector<std::vector<float>> output_buffers_single_precision(
|
||||
plugin->numOutputs);
|
||||
std::vector<std::vector<double>> output_buffers_double_precision(
|
||||
plugin->numOutputs);
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
auto request = read_object<AudioBuffers>(host_vst_process_replacing,
|
||||
process_buffer);
|
||||
// Let the plugin process the MIDI events that were received since
|
||||
// the last buffer, and then clean up those events. This approach
|
||||
// should not be needed but Kontakt only stores pointers to rather
|
||||
// than copies of the events.
|
||||
std::lock_guard lock(next_buffer_midi_events_mutex);
|
||||
|
||||
// Since the host should only be calling one of `process()`,
|
||||
// processReplacing()` or `processDoubleReplacing()`, we can all
|
||||
// handle them over the same socket. We pick which one to call
|
||||
// depending on the type of data we got sent and the plugin's
|
||||
// reported support for these functions.
|
||||
std::visit(
|
||||
overload{
|
||||
[&](std::vector<std::vector<float>>& input_buffers) {
|
||||
// The process functions expect a `float**` for their
|
||||
// inputs and their outputs
|
||||
std::vector<float*> inputs;
|
||||
for (auto& buffer : input_buffers) {
|
||||
inputs.push_back(buffer.data());
|
||||
}
|
||||
|
||||
// We reuse the buffers to avoid some unnecessary heap
|
||||
// allocations, so we need to make sure the buffers are
|
||||
// large enough since plugins can change their output
|
||||
// configuration. The type we're using here (single
|
||||
// precision floats vs double precisioon doubles) should
|
||||
// be the same as the one we're sending in our response.
|
||||
std::vector<float*> outputs;
|
||||
output_buffers_single_precision.resize(
|
||||
plugin->numOutputs);
|
||||
for (auto& buffer : output_buffers_single_precision) {
|
||||
buffer.resize(request.sample_frames);
|
||||
outputs.push_back(buffer.data());
|
||||
}
|
||||
|
||||
// Any plugin made in the last fifteen years or so
|
||||
// should support `processReplacing`. In the off chance
|
||||
// it does not we can just emulate this behavior
|
||||
// ourselves.
|
||||
if (plugin->processReplacing) {
|
||||
plugin->processReplacing(plugin, inputs.data(),
|
||||
outputs.data(),
|
||||
request.sample_frames);
|
||||
} else {
|
||||
// If we zero out this buffer then the behavior is
|
||||
// the same as `processReplacing``
|
||||
for (std::vector<float>& buffer :
|
||||
output_buffers_single_precision) {
|
||||
std::fill(buffer.begin(), buffer.end(), 0.0);
|
||||
}
|
||||
|
||||
plugin->process(plugin, inputs.data(),
|
||||
outputs.data(),
|
||||
request.sample_frames);
|
||||
}
|
||||
|
||||
AudioBuffers response{output_buffers_single_precision,
|
||||
request.sample_frames};
|
||||
write_object(host_vst_process_replacing, response,
|
||||
process_buffer);
|
||||
},
|
||||
[&](std::vector<std::vector<double>>& input_buffers) {
|
||||
// Exactly the same as the above, but for double
|
||||
// precision audio
|
||||
std::vector<double*> inputs;
|
||||
for (auto& buffer : input_buffers) {
|
||||
inputs.push_back(buffer.data());
|
||||
}
|
||||
|
||||
std::vector<double*> outputs;
|
||||
output_buffers_double_precision.resize(
|
||||
plugin->numOutputs);
|
||||
for (auto& buffer : output_buffers_double_precision) {
|
||||
buffer.resize(request.sample_frames);
|
||||
outputs.push_back(buffer.data());
|
||||
}
|
||||
|
||||
plugin->processDoubleReplacing(plugin, inputs.data(),
|
||||
outputs.data(),
|
||||
request.sample_frames);
|
||||
|
||||
AudioBuffers response{output_buffers_double_precision,
|
||||
request.sample_frames};
|
||||
write_object(host_vst_process_replacing, response,
|
||||
process_buffer);
|
||||
}},
|
||||
request.buffers);
|
||||
|
||||
next_audio_buffer_midi_events.clear();
|
||||
} catch (const boost::system::system_error&) {
|
||||
// The plugin has cut off communications, so we can shut down this
|
||||
// host application
|
||||
break;
|
||||
}
|
||||
}
|
||||
})(event);
|
||||
});
|
||||
}
|
||||
|
||||
intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin,
|
||||
@@ -419,7 +398,7 @@ intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin,
|
||||
// When hosting multiple plugins in a group process, all plugins
|
||||
// should get a unique window class
|
||||
const std::string window_class =
|
||||
"yabridge plugin " + socket_endpoint.path();
|
||||
"yabridge plugin " + sockets.base_dir.string();
|
||||
Editor& editor_instance = editor.emplace<Editor>(
|
||||
config, window_class, x11_handle, plugin);
|
||||
|
||||
@@ -570,19 +549,9 @@ intptr_t Vst2Bridge::host_callback(AEffect* effect,
|
||||
intptr_t value,
|
||||
void* data,
|
||||
float option) {
|
||||
// HACK: Sadly this is needed to work around a mutual recursion issue with
|
||||
// REAPER and Renoise. See #29 and #32.
|
||||
// TODO: We don't have access to the verbosity level here, but it would be
|
||||
// nice to log that this is being skipped when `YABRIDGE_DEBUG_LEVEL
|
||||
// >= 2`.
|
||||
if (config.hack_reaper_update_display &&
|
||||
opcode == audioMasterUpdateDisplay) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
HostCallbackDataConverter converter(effect, time_info);
|
||||
return send_event(vst_host_callback, host_callback_mutex, converter,
|
||||
std::nullopt, opcode, index, value, data, option);
|
||||
return sockets.vst_host_callback.send(converter, std::nullopt, opcode,
|
||||
index, value, data, option);
|
||||
}
|
||||
|
||||
intptr_t VST_CALL_CONV host_callback_proxy(AEffect* effect,
|
||||
@@ -594,18 +563,3 @@ intptr_t VST_CALL_CONV host_callback_proxy(AEffect* effect,
|
||||
return get_bridge_instance(effect).host_callback(effect, opcode, index,
|
||||
value, data, option);
|
||||
}
|
||||
|
||||
uint32_t WINAPI handle_dispatch_midi_events_proxy(void* instance) {
|
||||
static_cast<Vst2Bridge*>(instance)->handle_dispatch_midi_events();
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t WINAPI handle_parameters_proxy(void* instance) {
|
||||
static_cast<Vst2Bridge*>(instance)->handle_parameters();
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t WINAPI handle_process_replacing_proxy(void* instance) {
|
||||
static_cast<Vst2Bridge*>(instance)->handle_process_replacing();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <boost/asio/local/stream_protocol.hpp>
|
||||
#include <mutex>
|
||||
|
||||
#include "../../common/communication.h"
|
||||
#include "../../common/configuration.h"
|
||||
#include "../../common/logging.h"
|
||||
#include "../editor.h"
|
||||
@@ -46,7 +47,7 @@ struct EditorOpening {};
|
||||
* plugin and provides host callback function for the plugin to talk back.
|
||||
*
|
||||
* @remark Because of Win32 API limitations, all window handling has to be done
|
||||
* from the same thread. Most plugins won't have any issues when using
|
||||
* from a single thread. Most plugins won't have any issues when using
|
||||
* multiple message loops, but the Melda plugins for instance will only update
|
||||
* their GUIs from the message loop of the thread that created the first
|
||||
* instance. This is why we pass an IO context to this class so everything
|
||||
@@ -64,8 +65,8 @@ class Vst2Bridge {
|
||||
* also be run from this context.
|
||||
* @param plugin_dll_path A (Unix style) path to the VST plugin .dll file to
|
||||
* load.
|
||||
* @param socket_endpoint_path A (Unix style) path to the Unix socket
|
||||
* endpoint the native VST plugin created to communicate over.
|
||||
* @param endpoint_base_dir The base directory used for the socket
|
||||
* endpoints. See `Sockets` for more information.
|
||||
*
|
||||
* @note The object has to be constructed from the same thread that calls
|
||||
* `main_context.run()`.
|
||||
@@ -73,9 +74,9 @@ class Vst2Bridge {
|
||||
* @throw std::runtime_error Thrown when the VST plugin could not be loaded,
|
||||
* or if communication could not be set up.
|
||||
*/
|
||||
Vst2Bridge(boost::asio::io_context& main_context,
|
||||
Vst2Bridge(MainContext& main_context,
|
||||
std::string plugin_dll_path,
|
||||
std::string socket_endpoint_path);
|
||||
std::string endpoint_base_dir);
|
||||
|
||||
/**
|
||||
* Returns true if the message loop should be skipped. This happens when the
|
||||
@@ -127,14 +128,6 @@ class Vst2Bridge {
|
||||
*/
|
||||
void handle_win32_events();
|
||||
|
||||
// These functions are the entry points for the `*_handler` threads
|
||||
// defined below. They're defined here because we can't use lambdas with
|
||||
// WinAPI's `CreateThread` which is needed to support the proper call
|
||||
// conventions the VST plugins expect.
|
||||
void handle_dispatch_midi_events();
|
||||
void handle_parameters();
|
||||
void handle_process_replacing();
|
||||
|
||||
/**
|
||||
* Forward the host callback made by the plugin to the host and return the
|
||||
* results.
|
||||
@@ -149,6 +142,11 @@ class Vst2Bridge {
|
||||
*/
|
||||
std::optional<VstTimeInfo> time_info;
|
||||
|
||||
/**
|
||||
* The path to the .dll being loaded in the Wine VST host.
|
||||
*/
|
||||
const boost::filesystem::path vst_plugin_path;
|
||||
|
||||
private:
|
||||
/**
|
||||
* A wrapper around `plugin->dispatcher` that handles the opening and
|
||||
@@ -166,7 +164,7 @@ class Vst2Bridge {
|
||||
* message handling can be performed from a single thread, even when hosting
|
||||
* multiple plugins.
|
||||
*/
|
||||
boost::asio::io_context& io_context;
|
||||
MainContext& main_context;
|
||||
|
||||
/**
|
||||
* The configuration for this instance of yabridge based on the `.so` file
|
||||
@@ -188,49 +186,6 @@ class Vst2Bridge {
|
||||
*/
|
||||
AEffect* plugin;
|
||||
|
||||
/**
|
||||
* The UNIX domain socket endpoint used for communicating to this specific
|
||||
* bridged plugin.
|
||||
*/
|
||||
boost::asio::local::stream_protocol::endpoint socket_endpoint;
|
||||
|
||||
// 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;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The thread that specifically handles `effProcessEvents` opcodes so the
|
||||
* plugin can still receive MIDI during GUI interaction to work around Win32
|
||||
@@ -248,11 +203,13 @@ class Vst2Bridge {
|
||||
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.
|
||||
* All sockets used for communicating with this specific plugin.
|
||||
*
|
||||
* NOTE: This is defined **after** the threads on purpose. This way the
|
||||
* sockets will be closed first, and we can then safely wait for the
|
||||
* threads to exit.
|
||||
*/
|
||||
std::mutex host_callback_mutex;
|
||||
Sockets<Win32Thread> sockets;
|
||||
|
||||
/**
|
||||
* A scratch buffer for sending and receiving data during `process` and
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
* - Melda plugins when having multiple editor windows open within a single
|
||||
* plugin group
|
||||
*/
|
||||
constexpr int max_win32_messages = 20;
|
||||
constexpr int max_win32_messages [[maybe_unused]] = 20;
|
||||
|
||||
/**
|
||||
* Used to store the maximum width and height of a screen.
|
||||
|
||||
@@ -24,22 +24,9 @@
|
||||
#include "../common/utils.h"
|
||||
#include "bridges/vst2.h"
|
||||
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
/**
|
||||
* The delay between calls to the event loop at a more than cinematic 30 fps.
|
||||
*/
|
||||
constexpr std::chrono::duration event_loop_interval = 1000ms / 30;
|
||||
|
||||
/**
|
||||
* Handle both Win32 and X11 events on a timer. This is more or less a
|
||||
* simplified version of `GroupBridge::async_handle_events`.
|
||||
*/
|
||||
void async_handle_events(boost::asio::steady_timer& timer, Vst2Bridge& bridge);
|
||||
|
||||
/**
|
||||
* This is the default VST host application. It will load the specified VST2
|
||||
* plugin, and then connect back to the `libyabridge.so` instace that spawned
|
||||
* plugin, and then connect back to the `libyabridge.so` instance that spawned
|
||||
* this over the socket.
|
||||
*
|
||||
* The explicit calling convention is needed to work around a bug introduced in
|
||||
@@ -48,9 +35,9 @@ void async_handle_events(boost::asio::steady_timer& timer, Vst2Bridge& bridge);
|
||||
int __cdecl main(int argc, char* argv[]) {
|
||||
set_realtime_priority();
|
||||
|
||||
// We pass the name of the VST plugin .dll file to load and the Unix domain
|
||||
// socket to connect to in plugin/bridge.cpp as the first two arguments of
|
||||
// this process.
|
||||
// We pass the name of the VST plugin .dll file to load and the base
|
||||
// directory for the Unix domain socket endpoints to connect to as the first
|
||||
// two arguments of this process in plugin/bridge.cpp.
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: "
|
||||
#ifdef __i386__
|
||||
@@ -58,7 +45,7 @@ int __cdecl main(int argc, char* argv[]) {
|
||||
#else
|
||||
<< yabridge_individual_host_name
|
||||
#endif
|
||||
<< " <vst_plugin_dll> <unix_domain_socket>" << std::endl;
|
||||
<< " <vst_plugin_dll> <endpoint_base_directory>" << std::endl;
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -73,17 +60,16 @@ int __cdecl main(int argc, char* argv[]) {
|
||||
<< std::endl;
|
||||
|
||||
// As explained in `Vst2Bridge`, the plugin has to be initialized in the
|
||||
// same thread as the one that calls `io_context.run()`. And for some
|
||||
// reason, a lot of plugins have memory corruption issues when executing
|
||||
// `LoadLibrary()` or some of their functions from within a `std::thread`
|
||||
// (although the WinAPI `CreateThread()` does not have these issues). This
|
||||
// setup is slightly more convoluted than it has to be, but doing it this
|
||||
// way we don't need to differentiate between individually hosted plugins
|
||||
// and plugin groups when it comes to event handling.
|
||||
boost::asio::io_context io_context{};
|
||||
// same thread as the one that calls `io_context.run()`. This setup is
|
||||
// slightly more convoluted than it has to be, but doing it this way we
|
||||
// don't need to differentiate between individually hosted plugins and
|
||||
// plugin groups when it comes to event handling.
|
||||
// TODO: Update documentation once we figure out if we can safely replace
|
||||
// MainContext again with a normal `io_context`.
|
||||
MainContext main_context{};
|
||||
std::unique_ptr<Vst2Bridge> bridge;
|
||||
try {
|
||||
bridge = std::make_unique<Vst2Bridge>(io_context, plugin_dll_path,
|
||||
bridge = std::make_unique<Vst2Bridge>(main_context, plugin_dll_path,
|
||||
socket_endpoint_path);
|
||||
} catch (const std::runtime_error& error) {
|
||||
std::cerr << "Error while initializing Wine VST host:" << std::endl;
|
||||
@@ -97,29 +83,13 @@ int __cdecl main(int argc, char* argv[]) {
|
||||
|
||||
// We'll listen for `dispatcher()` calls on a different thread, but the
|
||||
// actual events will still be executed within the IO context
|
||||
std::jthread dispatch_handler([&]() { bridge->handle_dispatch(); });
|
||||
Win32Thread dispatch_handler([&]() { bridge->handle_dispatch(); });
|
||||
|
||||
// Handle Win32 messages and X11 events on a timer, just like in
|
||||
// `GroupBridge::async_handle_events()``
|
||||
boost::asio::steady_timer events_timer(io_context);
|
||||
async_handle_events(events_timer, *bridge);
|
||||
|
||||
io_context.run();
|
||||
}
|
||||
|
||||
void async_handle_events(boost::asio::steady_timer& timer, Vst2Bridge& bridge) {
|
||||
// Try to keep a steady framerate, but add in delays to let other events get
|
||||
// handled if the GUI message handling somehow takes very long.
|
||||
timer.expires_at(std::max(timer.expiry() + event_loop_interval,
|
||||
std::chrono::steady_clock::now() + 5ms));
|
||||
timer.async_wait([&](const boost::system::error_code& error) {
|
||||
if (error.failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bridge.handle_x11_events();
|
||||
bridge.handle_win32_events();
|
||||
|
||||
async_handle_events(timer, bridge);
|
||||
main_context.async_handle_events([&]() {
|
||||
bridge->handle_x11_events();
|
||||
bridge->handle_win32_events();
|
||||
});
|
||||
main_context.run();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,38 @@
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
MainContext::MainContext() : context(), events_timer(context) {}
|
||||
|
||||
void MainContext::run() {
|
||||
context.run();
|
||||
}
|
||||
|
||||
void MainContext::stop() {
|
||||
context.stop();
|
||||
}
|
||||
|
||||
uint32_t WINAPI
|
||||
win32_thread_trampoline(fu2::unique_function<void()>* entry_point) {
|
||||
(*entry_point)();
|
||||
delete entry_point;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Win32Thread::Win32Thread(Win32Thread&& o) : handle(std::move(o.handle)) {}
|
||||
|
||||
Win32Thread& Win32Thread::operator=(Win32Thread&& o) {
|
||||
handle = std::move(o.handle);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
Win32Thread::~Win32Thread() {
|
||||
if (handle) {
|
||||
WaitForSingleObject(handle.get(), INFINITE);
|
||||
}
|
||||
}
|
||||
|
||||
Win32Thread::Win32Thread() : handle(nullptr, nullptr) {}
|
||||
|
||||
Win32Timer::Win32Timer(HWND window_handle,
|
||||
|
||||
+145
-22
@@ -16,6 +16,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "boost-fix.h"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
@@ -26,24 +28,119 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <function2/function2.hpp>
|
||||
|
||||
/**
|
||||
* A simple RAII wrapper around the Win32 thread API.
|
||||
* The delay between calls to the event loop at an even more than cinematic 30
|
||||
* fps.
|
||||
*/
|
||||
constexpr std::chrono::duration event_loop_interval =
|
||||
std::chrono::milliseconds(1000) / 30;
|
||||
|
||||
/**
|
||||
* A wrapper around `boost::asio::io_context()` to serve as the application's
|
||||
* main IO context. A single instance is shared for all plugins in a plugin
|
||||
* group so that several important events can be handled on the main thread,
|
||||
* which can be required because in the Win32 model all GUI related operations
|
||||
* have to be handled from the same thread. This will be run from the
|
||||
* application's main thread.
|
||||
*/
|
||||
class MainContext {
|
||||
public:
|
||||
MainContext();
|
||||
|
||||
/**
|
||||
* Run the IO context. This rest of this class assumes that this is only
|
||||
* done from a single thread.
|
||||
*/
|
||||
void run();
|
||||
|
||||
/**
|
||||
* Drop all future work from the IO context. This does not necessarily mean
|
||||
* that the thread that called `main_context.run()` immediatly returns.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Start a timer to handle events every `event_loop_interval` milliseconds.
|
||||
* `message_loop_active()` will return `true` while `handler` is being
|
||||
* executed.
|
||||
*
|
||||
* @param handler The function that should be executed in the IO context
|
||||
* when the timer ticks. This should be a function that handles both the
|
||||
* X11 events and the Win32 message loop.
|
||||
*/
|
||||
template <typename F>
|
||||
void async_handle_events(F handler) {
|
||||
// Try to keep a steady framerate, but add in delays to let other events
|
||||
// get handled if the GUI message handling somehow takes very long.
|
||||
events_timer.expires_at(std::max(
|
||||
events_timer.expiry() + event_loop_interval,
|
||||
std::chrono::steady_clock::now() + std::chrono::milliseconds(5)));
|
||||
events_timer.async_wait(
|
||||
[&, handler](const boost::system::error_code& error) {
|
||||
if (error.failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
event_loop_active = true;
|
||||
handler();
|
||||
event_loop_active = false;
|
||||
|
||||
async_handle_events(handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Is `true` if the context is currently handling the Win32 message loop and
|
||||
* incoming `dispatch()` events should be handled on their own thread (as
|
||||
* posting them to the IO context will thus block).
|
||||
*
|
||||
* TODO: No longer used after the thread rework, we can probably just drop
|
||||
* this if everything works out
|
||||
*/
|
||||
std::atomic_bool event_loop_active;
|
||||
|
||||
/**
|
||||
* The raw IO context. Can and should be used directly for everything that's
|
||||
* not the event handling loop.
|
||||
*/
|
||||
boost::asio::io_context context;
|
||||
|
||||
private:
|
||||
/**
|
||||
* The timer used to periodically handle X11 events and Win32 messages.
|
||||
*/
|
||||
boost::asio::steady_timer events_timer;
|
||||
};
|
||||
|
||||
/**
|
||||
* A proxy function that calls `Win32Thread::entry_point` since `CreateThread()`
|
||||
* is not usable with lambdas directly. Calling the passed function will invoke
|
||||
* the lambda with the arguments passed during `Win32Thread`'s constructor. This
|
||||
* function deallocates the function after it's finished executing.
|
||||
*
|
||||
* These threads are implemented using `CreateThread` rather than `std::thread`
|
||||
* because in some cases `std::thread` in winelib causes very hard to debug data
|
||||
* races within plugins such as Serum. This might be caused by calling
|
||||
* conventions being handled differently.
|
||||
* We can't store the function pointer in the `Win32Thread` object because
|
||||
* moving a `Win32Thread` object would then cause issues.
|
||||
*
|
||||
* This somewhat mimicks `std::thread`, with the following differences:
|
||||
* @param entry_point A `fu2::unique_function<void()>*` pointer to a function
|
||||
* pointer, great.
|
||||
*/
|
||||
uint32_t WINAPI
|
||||
win32_thread_trampoline(fu2::unique_function<void()>* entry_point);
|
||||
|
||||
/**
|
||||
* A simple RAII wrapper around the Win32 thread API that imitates
|
||||
* `std::jthread`, including implicit joining (or waiting, since this is Win32)
|
||||
* on destruction.
|
||||
*
|
||||
* - The threads will immediatly be killed silently when a `Win32Thread` object
|
||||
* goes out of scope. This is the desired behavior in our case since the host
|
||||
* will have already saved chunk data before closing the plugin and this
|
||||
* ensures that the plugin shuts down quickly.
|
||||
* - This does not accept lambdas because we're calling a C function that
|
||||
* expects a function pointer of type `LPTHREAD_START_ROUTINE`. GCC supports
|
||||
* converting stateless lambdas to this format, but clang (as used for IDE
|
||||
* tooling) does not.
|
||||
* `std::thread` uses pthreads directly in Winelib (since this is technically a
|
||||
* regular Linux application). This means that when using
|
||||
* `std::thread`/`std::jthread` directly, some thread local information that
|
||||
* `CreateThread()` would normally set does not get initialized. This could then
|
||||
* lead to memory errors. This wrapper aims to be equivalent to `std::jthread`,
|
||||
* but using the Win32 API instead.
|
||||
*
|
||||
* @note This should be used instead of `std::thread` or `std::jthread` whenever
|
||||
* the thread directly calls third party library code, i.e. `LoadLibrary()`,
|
||||
@@ -58,18 +155,44 @@ class Win32Thread {
|
||||
Win32Thread();
|
||||
|
||||
/**
|
||||
* Constructor that immediately starts running the thread
|
||||
* Constructor that immediately starts running the thread. This works
|
||||
* equivalently to `std::jthread`.
|
||||
*
|
||||
* @param entry_point The thread entry point that should be run.
|
||||
* @param parameter The parameter passed to the entry point function.
|
||||
|
||||
* @tparam F A function type that should be convertible to a
|
||||
* `LPTHREAD_START_ROUTINE` function pointer.
|
||||
*/
|
||||
template <typename F>
|
||||
Win32Thread(F entry_point, void* parameter)
|
||||
: handle(CreateThread(nullptr, 0, entry_point, parameter, 0, nullptr),
|
||||
CloseHandle) {}
|
||||
template <typename Function, typename... Args>
|
||||
Win32Thread(Function&& f, Args&&... args)
|
||||
: handle(
|
||||
CreateThread(
|
||||
nullptr,
|
||||
0,
|
||||
reinterpret_cast<LPTHREAD_START_ROUTINE>(
|
||||
win32_thread_trampoline),
|
||||
// `std::function` does not support functions with move
|
||||
// captures the function has to be copy-constructable.
|
||||
// Function2's unique_function lets us capture and move our
|
||||
// arguments to the lambda so we don't end up with dangling
|
||||
// references.
|
||||
new fu2::unique_function<void()>(
|
||||
[f = std::move(f), ... args = std::move(args)]() mutable {
|
||||
f(std::move(args)...);
|
||||
}),
|
||||
0,
|
||||
nullptr),
|
||||
CloseHandle) {}
|
||||
|
||||
/**
|
||||
* Join (or wait on, since this is WIn32) the thread on shutdown, just like
|
||||
* `std::jthread` does.
|
||||
*/
|
||||
~Win32Thread();
|
||||
|
||||
Win32Thread(const Win32Thread&) = delete;
|
||||
Win32Thread& operator=(const Win32Thread&) = delete;
|
||||
|
||||
Win32Thread(Win32Thread&&);
|
||||
Win32Thread& operator=(Win32Thread&&);
|
||||
|
||||
private:
|
||||
/**
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
[wrap-file]
|
||||
directory = function2-4.1.0
|
||||
|
||||
source_url = https://github.com/Naios/function2/archive/4.1.0.tar.gz
|
||||
source_filename = function2-4.1.0.tar.gz
|
||||
source_hash = c3aaeaf93bf90c0f4505a18f1094b51fe28881ce202c3bf78ec4efb336c51981
|
||||
|
||||
patch_url = file:./subprojects/function2-patch-4.1.0.tar.xz
|
||||
patch_filename = function2-patch-4.1.0.tar.xz
|
||||
patch_hash = 4b966afd862413ea1f3d96484e74401992ec958f1ee2b4cc161f3cb7c36fe7ba
|
||||
@@ -31,9 +31,9 @@ use textwrap::Wrapper;
|
||||
use crate::config::{Config, KnownConfig};
|
||||
|
||||
/// (Part of) the expected output when running `yabridge-host.exe`. Used to verify that everything's
|
||||
/// working correctly.
|
||||
const YABRIDGE_HOST_EXPECTED_OUTPUT: &str =
|
||||
"Usage: yabridge-host.exe <vst_plugin_dll> <unix_domain_socket>";
|
||||
/// working correctly. We'll only match this prefix so we can modify the exact output at a later
|
||||
/// moment without causing issues.
|
||||
const YABRIDGE_HOST_EXPECTED_OUTPUT_PREFIX: &str = "Usage: yabridge-";
|
||||
|
||||
/// Wrapper around [`std::fs::copy()`](std::fs::copy) with a human readable error message.
|
||||
pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
|
||||
@@ -220,7 +220,7 @@ pub fn verify_wine_setup(config: &mut Config) -> Result<()> {
|
||||
let mut success = false;
|
||||
let mut last_error: Option<&str> = None;
|
||||
for line in stderr.lines() {
|
||||
if line == YABRIDGE_HOST_EXPECTED_OUTPUT {
|
||||
if line.starts_with(YABRIDGE_HOST_EXPECTED_OUTPUT_PREFIX) {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user