mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-07 03:50:11 +02:00
74c3cab046
Now all pieces are in place to allow handling events over multiple socket connections.
169 lines
9.8 KiB
Markdown
169 lines
9.8 KiB
Markdown
# 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
|
|
`yabridge-host-32.exe`/`yabridge-host-32.exe.so` if the bitbirdge is enabled).
|
|
I'll refer to the copy of or the symlink to `libyabridge.so` as _the plugin_,
|
|
the native Linux VST host that's hosting the plugin as _the native VST host_,
|
|
the Wine VST host application that's hosting a Windows `.dll` file as _the Wine
|
|
VST host_, and the Windows VST plugin that's being loaded in the Wine VST host
|
|
as the _Windows VST plugin_. The whole process works as follows:
|
|
|
|
1. Some copy of or a symlink to `libyabridge.so` gets loaded as a VST plugin in
|
|
a Linux VST host. This file should have been renamed to match a Windows VST
|
|
plugin `.dll` file in the same directory. For instance, if there's a
|
|
`Serum_x64.dll` file you'd like to bridge, then there should be a symlink to
|
|
`libyabridge.so` named `Serum_x64.so`.
|
|
2. The plugin first attempts to locate and determine:
|
|
|
|
- The Windows VST plugin `.dll` file that should be loaded.
|
|
|
|
- The architecture of that VST plugin file. This is done by inspecting the
|
|
headers if the `.dll` file.
|
|
|
|
- The location of the Wine VST host. This will depend on the architecture
|
|
detected for the plugin. If the plugin was compiled for the `x86_64`
|
|
architecture or the 'Any CPU' target, then we will look for
|
|
`yabridge-host.exe`. If the plugin was compiled for the `x86` architecture,
|
|
when we'll search for `yabridge-host-32.exe`.
|
|
|
|
We will first search for this file alongside the actual location of
|
|
`libyabridge.so`. This is useful for development, as it allows you to use a
|
|
symlink to `libyabridge.so` directly from the build directory causing
|
|
yabridge to automatically pick up the right version of the Wine VST host.
|
|
If this file cannot be found, then it will fall back to searching through
|
|
the search path.
|
|
|
|
- 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 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 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.
|
|
|
|
- Calls from the native VST host to the plugin's `dispatcher()` function with
|
|
the `effProcessEvents` opcode. These also get forwarded to the Windows VST
|
|
plugin through the Wine VST host. This has to be handled separately from
|
|
all other events because of limitations of the Win32 API. Without doing
|
|
this the plugin would not be able to receive any MIDI events while the GUI
|
|
is being resized or a dropdown menu or message box is shown.
|
|
|
|
- Host callback calls from the Windows VST plugin through the
|
|
`audioMasterCallback` function. These get forwarded to the native VST host
|
|
through the plugin.
|
|
|
|
Both the `dispatcher()` and `audioMasterCallback()` functions are handled
|
|
in the same way, with some minor variations on how payload data gets
|
|
serialized depending on the opcode of the event being sent. See the section
|
|
below this for more details on this procedure.
|
|
|
|
- Calls from the native VST host to the plugin's `getParameter()` and
|
|
`setParameter()` functions. Both functions get forwarded to the Windows VST
|
|
plugin through the Wine VST host using a single socket because they're very
|
|
similar and don't need any complicated behaviour.
|
|
|
|
- Calls from the native VST host to the plugin's `processReplacing()` and
|
|
`processDoubleReplacing()` functions. These functions get forwarded to the
|
|
Windows VST plugin through the Wine VST host. In the rare event that the
|
|
plugin does not support `processReplacing()` and only supports The
|
|
deprecated commutative `process()` function, then the Wine VST host will
|
|
emulate the behavior of `processReplacing()` instead. Single and double
|
|
precision audio go over the same socket since the host will only call one
|
|
or the other, and we just use a variant to determine which one should be
|
|
called on the Wine host side.
|
|
|
|
- And finally there's a separate socket for control messages. At the moment
|
|
this is only used to transfer the Windows VST plugin's `AEffect` object to
|
|
the plugin and the current configuration from the plugin to the Wine VST
|
|
host on startup.
|
|
|
|
The operations described above involving the host -> plugin `dispatcher()`and
|
|
plugin -> host `audioMaster()` functions are all handled by first serializing
|
|
the function parameters and any payload data into a binary format so they can
|
|
be sent over a socket. The objects used for encoding both the requests and
|
|
the responses for theses events can be found in `src/common/serialization.h`,
|
|
and the functions that actually read and write these objects over the sockets
|
|
are located in `src/common/communication.h`. The actual binary serialization
|
|
is handled using [bitsery](https://github.com/fraillt/bitsery).
|
|
|
|
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.
|
|
7. After the Windows VST plugin has started loading we will forward all values
|
|
from the Windows VST plugin's `AEffect` struct to the plugin, and the plugins
|
|
configuration gets sent back over the same socket to the Wine VST host. After
|
|
this point the plugin will stop blocking and the initialization process is
|
|
finished.
|
|
|
|
## Plugin groups
|
|
|
|
When using plugin groups, the startup and event handling behavior is slightly
|
|
different.
|
|
|
|
- First of all, instead of directly spawning a Wine process to host the plugin,
|
|
yabridge will either:
|
|
|
|
- Connect to an existing group host process that matches the plugin's
|
|
combination of group name, Wine prefix, and Windows VST plugin architecture,
|
|
and ask it to host the Windows VST plugin.
|
|
- Spawn a new group process and detach it from the process, then proceed as
|
|
normal by connecting to that process as described above. When two yabridge
|
|
instances are initialized simultaneously and both try to launch a new group
|
|
process, then the process that manages to listen on the group's socket first
|
|
will handle both instances.
|
|
|
|
- Events, both Win32 messages and `dispatcher()` events, are handled slightly
|
|
differently when using plugin groups. Because most of the Win32 API cannot be
|
|
used from multiple threads, all plugin initialization and all event handling
|
|
has to be done from the same thread. To achieve this, yabridge will use a
|
|
slightly modified version of the `dispatcher()` handler that executes the
|
|
actual events for all plugins within a single Boost.Asio IO context.
|
|
- Win32 messages are now also handled on a timer within the same IO context so
|
|
mentioned above. This behavior is different from individually hosted plugins,
|
|
where the message loop can simply be run after every event. If any of the
|
|
plugins within the plugin group is in a state that would cause the message
|
|
loop to fail, such as when a plugin is in the process of opening its editor
|
|
GUI, then the message loop will be skipped temporarily.
|