mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-07 03:50:11 +02:00
Merge branch 'feature/plugin-groups'
This commit is contained in:
@@ -14,6 +14,11 @@ on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
# This otherwise gets run under dash which does not support brace expansion
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build-bionic:
|
||||
name: Build on Ubuntu 18.04
|
||||
@@ -48,11 +53,8 @@ jobs:
|
||||
ninja -C build
|
||||
- name: Create an archive for the binaries
|
||||
run: |
|
||||
set -e
|
||||
|
||||
mkdir yabridge
|
||||
# This is ran under dash which does not support brace expansion
|
||||
cp build/libyabridge.so build/yabridge-host.exe build/yabridge-host.exe.so build/yabridge-host-32.exe build/yabridge-host-32.exe.so yabridge
|
||||
cp build/libyabridge.so build/yabridge-{host,group}{,-32}.exe{,.so} yabridge
|
||||
cp CHANGELOG.md README.md yabridge
|
||||
|
||||
tar -caf "$ARCHIVE_NAME" yabridge
|
||||
@@ -91,11 +93,8 @@ jobs:
|
||||
ninja -C build
|
||||
- name: Create an archive for the binaries
|
||||
run: |
|
||||
set -e
|
||||
|
||||
mkdir yabridge
|
||||
# This is ran under dash which does not support brace expansion
|
||||
cp build/libyabridge.so build/yabridge-host.exe build/yabridge-host.exe.so build/yabridge-host-32.exe build/yabridge-host-32.exe.so yabridge
|
||||
cp build/libyabridge.so build/yabridge-{host,group}{,-32}.exe{,.so} yabridge
|
||||
cp CHANGELOG.md README.md yabridge
|
||||
|
||||
tar -caf "$ARCHIVE_NAME" yabridge
|
||||
|
||||
+17
-1
@@ -8,10 +8,26 @@ Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added the ability to host multiple plugins in the same Wine process through
|
||||
_plugin groups_. A plugin group is a user-defined set of plugins that will be
|
||||
hosted together in the same Wine process. This allows multiple instances of
|
||||
plugins to share data and communicate with eachother. Examples of plugins that
|
||||
can benefit from this are Fabfilter Pro-Q 3, MMultiAnalyzer and the iZotope
|
||||
mixing plugins. See the readme for instructions on how to set this up.
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed architecture to use one less socket.
|
||||
- Removed dependency on 32-bit Boost.Filesystem.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Steal keyboard focus when clicking on the plugin editor window to account for
|
||||
the new keyboard focus behavior in Bitwig Studio 3.2.
|
||||
- Fixed large amount of empty lines in the log file when the Wine process closes
|
||||
unexpectedly.
|
||||
- Made the plugin and host detection slightly more robust.
|
||||
|
||||
## [1.1.4] - 2020-05-12
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ easy to debug and maintain.
|
||||
## Tested with
|
||||
|
||||
Yabridge has been verified to work correctly in the following VST hosts using
|
||||
Wine Staging 5.5 and 5.6:
|
||||
Wine Staging 5.8:
|
||||
|
||||
- Bitwig Studio 3.1 and the beta releases of 3.2
|
||||
- Carla 2.1
|
||||
@@ -125,6 +125,61 @@ yabridge is also able to load 32-bit VST plugins. The installation procedure for
|
||||
automatically detect whether a plugin is 32-bit or 64-bit on startup and it will
|
||||
handle it accordingly.
|
||||
|
||||
### Plugin groups
|
||||
|
||||
Some plugins have the ability to communicate with other instances of that same
|
||||
plugin or with other plugins made by the same manufacturer. This is often used
|
||||
in mixing plugins to allow different tracks to reference each other without
|
||||
having to route audio between them. Examples of plugins that do this are
|
||||
Fabfilter Pro-Q 3, MMultiAnalyzer and the iZotope mixing plugins. For this to
|
||||
work, all instances of a particular plugin have to be hosted in the same
|
||||
process.
|
||||
|
||||
Yabridge has the concept of _plugin groups_, which are user defined groups of
|
||||
plugins that will all be hosted in the same process. These plugins groups can be
|
||||
configured using a `yabridge.toml` file located either in the same directory as
|
||||
the symlink of or copy to `libyabridge.so`, or in any directories above it. This
|
||||
file contains case sensitive
|
||||
[glob](https://www.man7.org/linux/man-pages/man7/glob.7.html) patterns that are
|
||||
used to match the names of `*.so` files relative to that `yabridge.toml` file.
|
||||
These patterns can also match an entire directory. For simplicity's sake only
|
||||
the first `yabridge.toml` file found and only the first glob pattern matched
|
||||
within that file are considered. An example `yabridge.toml` file looks like
|
||||
this:
|
||||
|
||||
```toml
|
||||
# ~/.wine/drive_c/Program Files/Steinberg/VstPlugins/yabridge.toml
|
||||
|
||||
["FabFilter Pro-Q 3.so"]
|
||||
group = "fabfilter"
|
||||
|
||||
["MeldaProduction/Tools/MMultiAnalyzer.so"]
|
||||
group = "melda"
|
||||
|
||||
# Matches an entire directory and all files inside it. Make sure to not include
|
||||
# a trailing slash.
|
||||
["ToneBoosters"]
|
||||
group = "toneboosters"
|
||||
|
||||
["iZotope*/Neutron *"]
|
||||
group = "izotope"
|
||||
|
||||
["iZotope7/Insight 2.so"]
|
||||
group = "izotope"
|
||||
|
||||
# This won't do anything, since the pattern above has already matched this file
|
||||
["iZotope7/Neutron 2 Mix Tap.so"]
|
||||
group = "This will be ignored!"
|
||||
|
||||
# Don't do this! This matches all plugins in this directory and all of its
|
||||
# subdirectories, causing all of them to be hosted in a single process. While
|
||||
# this would increase startup performance considerably, it will also break any
|
||||
# form of individual plugin sandboxing provided by the host and could
|
||||
# potentially introduce all kinds of weird issues.
|
||||
# ["*"]
|
||||
# group = "all"
|
||||
```
|
||||
|
||||
### Wine prefixes
|
||||
|
||||
It is also possible to use yabridge with multiple Wine prefixes. Yabridge will
|
||||
@@ -188,12 +243,6 @@ Aside from that, these are some known caveats:
|
||||
- Most recent **iZotope** plugins don't have a functional GUI in a typical out
|
||||
of the box Wine setup because of missing dependencies. Please let me know if
|
||||
you know which dependencies are needed for these plugins to render correctly.
|
||||
- Some plugins, such as **Fabfilter Pro-Q 3**, are able to communicate between
|
||||
different instances of the same plugin by relying on the fact that they're all
|
||||
loaded into the same process. Right now this is something that yabridge does
|
||||
not do as it would break any form of sandboxing, meaning that if one plugin
|
||||
were to crash, all other plugins would go down with it. If this is something
|
||||
you need for your workflow, please let me know.
|
||||
|
||||
There are also some VST2.X extension features that have not been implemented yet
|
||||
because I haven't needed them myself. Let me know if you need any of these
|
||||
@@ -222,6 +271,7 @@ the following dependencies:
|
||||
The following dependencies are included in the repository as a Meson wrap:
|
||||
|
||||
- bitsery
|
||||
- tomlplusplus
|
||||
|
||||
The project can then be compiled as follows:
|
||||
|
||||
@@ -239,7 +289,7 @@ It is also possible to compile a host application for yabridge that's compatible
|
||||
with 32-bit plugins such as old SynthEdit plugins. This will allow yabridge to
|
||||
act as a bitbirdge, allowing you to run old 32-bit only Windows VST2 plugins in
|
||||
a modern 64-bit Linux VST host. For this you'll need to have installed the 32
|
||||
bit version of the XCB library. This can then be set up as follows:
|
||||
bit versions of the Boost and XCB libraries. This can then be set up as follows:
|
||||
|
||||
```shell
|
||||
# Enable the bitbridge on an existing build
|
||||
@@ -309,124 +359,3 @@ modifications in `src/plugin/plugin-bridge.cpp`. To enable this, simply run:
|
||||
```shell
|
||||
meson configure build --buildtype=debug -Duse-winedbg=true
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
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 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.
|
||||
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:
|
||||
|
||||
- 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()`
|
||||
function. This function gets forwarded to the Windows VST plugin through
|
||||
the Wine VST. 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.
|
||||
|
||||
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).
|
||||
|
||||
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
|
||||
`WineBridge::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 plugin's `AEffect` struct to the Linux native VST plugin over the
|
||||
`dispatcher()` socket. This is only done once at startup. After this point
|
||||
the plugin will stop blocking and has finished loading.
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# Architecture
|
||||
|
||||
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 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.
|
||||
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:
|
||||
|
||||
- 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()`
|
||||
function. This function gets forwarded to the Windows VST plugin through
|
||||
the Wine VST. 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.
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
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 plugin's `AEffect` struct to the Linux native VST plugin over the
|
||||
`dispatcher()` socket. This is only done once at startup. After this point
|
||||
the plugin will stop blocking and has finished loading.
|
||||
|
||||
## 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.
|
||||
+100
-18
@@ -15,15 +15,17 @@ if not meson.get_compiler('cpp').compiles(winelib_check)
|
||||
error('You need to set up a cross compiler, check the README for compilation instructions.')
|
||||
endif
|
||||
|
||||
# Depending on the `use-bitbridge` flag we'll enable building a second 32-bit
|
||||
# host application that can act as a bit bridge for using 32-bit Windows plugins
|
||||
# Depending on the `use-bitbridge` flag we'll enable building secondary 32-bit
|
||||
# host applications that can act as a bit bridge for using 32-bit Windows plugins
|
||||
# in 64-bit Linux VST hosts. The plugin will determine which host application to
|
||||
# use based on the `.dll` file it's trying to load.
|
||||
# This setup is necessary until Meson provides a way to have multiple
|
||||
# cross-builds for a single build directory:
|
||||
# https://github.com/mesonbuild/meson/issues/5125
|
||||
host_name_64bit = 'yabridge-host'
|
||||
host_name_32bit = 'yabridge-host-32'
|
||||
individual_host_name_64bit = 'yabridge-host'
|
||||
individual_host_name_32bit = 'yabridge-host-32'
|
||||
group_host_name_64bit = 'yabridge-group'
|
||||
group_host_name_32bit = 'yabridge-group-32'
|
||||
|
||||
# This provides an easy way to start the Wine VST host using winedbg since it
|
||||
# can be quite a pain to set up
|
||||
@@ -39,13 +41,13 @@ endif
|
||||
# and the name of the host binary
|
||||
subdir('src/common/config')
|
||||
|
||||
# Statically link against Boost.Filesystem, otherwise it becomes impossible to
|
||||
# distribute a prebuilt version of yabridge. For the Wine host applications we
|
||||
# only use the headers only libraries.
|
||||
# Statically link against Boost.Filesystem, otherwise it would become impossible
|
||||
# to distribute a prebuilt version of yabridge
|
||||
boost_dep = dependency('boost', version : '>=1.66', static : true)
|
||||
boost_filesystem_dep = dependency('boost', version : '>=1.66', modules : ['filesystem'], static : true)
|
||||
bitsery_dep = subproject('bitsery').get_variable('bitsery_dep')
|
||||
threads_dep = dependency('threads')
|
||||
tomlplusplus_dep = subproject('tomlplusplus').get_variable('tomlplusplus_dep')
|
||||
# The built in threads dependency does not know how to handle winegcc
|
||||
wine_threads_dep = declare_dependency(link_args : '-lpthread')
|
||||
xcb_dep = dependency('xcb')
|
||||
@@ -62,6 +64,7 @@ shared_library(
|
||||
[
|
||||
'src/common/logging.cpp',
|
||||
'src/common/serialization.cpp',
|
||||
'src/plugin/configuration.cpp',
|
||||
'src/plugin/plugin.cpp',
|
||||
'src/plugin/plugin-bridge.cpp',
|
||||
'src/plugin/utils.cpp',
|
||||
@@ -69,7 +72,13 @@ shared_library(
|
||||
],
|
||||
native : true,
|
||||
include_directories : include_dir,
|
||||
dependencies : [boost_dep, boost_filesystem_dep, bitsery_dep, threads_dep],
|
||||
dependencies : [
|
||||
boost_dep,
|
||||
boost_filesystem_dep,
|
||||
bitsery_dep,
|
||||
threads_dep,
|
||||
tomlplusplus_dep
|
||||
],
|
||||
cpp_args : compiler_options,
|
||||
link_args : ['-ldl']
|
||||
)
|
||||
@@ -77,20 +86,49 @@ shared_library(
|
||||
host_sources = [
|
||||
'src/common/logging.cpp',
|
||||
'src/common/serialization.cpp',
|
||||
'src/wine-host/bridges/vst2.cpp',
|
||||
'src/wine-host/editor.cpp',
|
||||
'src/wine-host/editor.cpp',
|
||||
'src/wine-host/vst-host.cpp',
|
||||
'src/wine-host/wine-bridge.cpp',
|
||||
'src/wine-host/utils.cpp',
|
||||
version_header,
|
||||
]
|
||||
|
||||
individual_host_sources = host_sources + ['src/wine-host/individual-host.cpp']
|
||||
group_host_sources = host_sources + [
|
||||
'src/wine-host/bridges/group.cpp',
|
||||
'src/wine-host/group-host.cpp',
|
||||
]
|
||||
|
||||
executable(
|
||||
host_name_64bit,
|
||||
host_sources,
|
||||
individual_host_name_64bit,
|
||||
individual_host_sources,
|
||||
native : false,
|
||||
include_directories : include_dir,
|
||||
dependencies : [boost_dep, bitsery_dep, wine_threads_dep, xcb_dep],
|
||||
dependencies : [
|
||||
boost_dep,
|
||||
boost_filesystem_dep,
|
||||
bitsery_dep,
|
||||
tomlplusplus_dep,
|
||||
wine_threads_dep,
|
||||
xcb_dep
|
||||
],
|
||||
cpp_args : compiler_options + ['-m64'],
|
||||
link_args : ['-m64']
|
||||
)
|
||||
|
||||
executable(
|
||||
group_host_name_64bit,
|
||||
group_host_sources,
|
||||
native : false,
|
||||
include_directories : include_dir,
|
||||
dependencies : [
|
||||
boost_dep,
|
||||
boost_filesystem_dep,
|
||||
bitsery_dep,
|
||||
tomlplusplus_dep,
|
||||
wine_threads_dep,
|
||||
xcb_dep
|
||||
],
|
||||
cpp_args : compiler_options + ['-m64'],
|
||||
link_args : ['-m64']
|
||||
)
|
||||
@@ -98,17 +136,44 @@ executable(
|
||||
if get_option('use-bitbridge')
|
||||
message('Bitbridge enabled, configuring a 32-bit host application')
|
||||
|
||||
# I honestly have no idea what the correct way to have `find_dependency()` use
|
||||
# `/usr/lib32` instead of `/usr/lib` is. If anyone does know, please tell me!
|
||||
# I honestly have no idea what the correct way is to have `dependency()` or
|
||||
# `compiler.find_dependency()` search for 32-bit versions of libraries when
|
||||
# cross-compiling. Meson also doesn't seem to respect the default linker
|
||||
# search path set by the system in `find_library()`. If anyone does know how
|
||||
# to properly do this, please let me know!
|
||||
winegcc = meson.get_compiler('cpp', native : false)
|
||||
boost_filesystem_dep = winegcc.find_library(
|
||||
'boost_filesystem',
|
||||
static : true,
|
||||
dirs : [
|
||||
# Used by Arch based distros
|
||||
'/usr/local/lib32',
|
||||
'/usr/lib32',
|
||||
# Used by Debian based distros
|
||||
'/usr/local/lib/i386-linux-gnu',
|
||||
'/usr/lib/i386-linux-gnu',
|
||||
# Used by Red Hat based distros, could cause issues though since Meson
|
||||
# cannot differentiate between the 32-bit version and the regular 64-bit
|
||||
# version that would normally be in /lib
|
||||
'/usr/local/lib',
|
||||
'/usr/lib',
|
||||
]
|
||||
)
|
||||
xcb_dep = winegcc.find_library('xcb')
|
||||
|
||||
executable(
|
||||
host_name_32bit,
|
||||
host_sources,
|
||||
individual_host_name_32bit,
|
||||
individual_host_sources,
|
||||
native : false,
|
||||
include_directories : include_dir,
|
||||
dependencies : [boost_dep, bitsery_dep, wine_threads_dep, xcb_dep],
|
||||
dependencies : [
|
||||
boost_dep,
|
||||
boost_filesystem_dep,
|
||||
bitsery_dep,
|
||||
tomlplusplus_dep,
|
||||
wine_threads_dep,
|
||||
xcb_dep
|
||||
],
|
||||
# FIXME: 32-bit winegcc defines `__stdcall` differently than the 64-bit
|
||||
# version, and one of the changes is the inclusion of
|
||||
# `__atribute__((__force_align_arg_pointer__))`. For whetever reason
|
||||
@@ -119,4 +184,21 @@ if get_option('use-bitbridge')
|
||||
cpp_args : compiler_options + ['-m32', '-Wno-ignored-attributes'],
|
||||
link_args : ['-m32']
|
||||
)
|
||||
|
||||
executable(
|
||||
group_host_name_32bit,
|
||||
group_host_sources,
|
||||
native : false,
|
||||
include_directories : include_dir,
|
||||
dependencies : [
|
||||
boost_dep,
|
||||
boost_filesystem_dep,
|
||||
bitsery_dep,
|
||||
tomlplusplus_dep,
|
||||
wine_threads_dep,
|
||||
xcb_dep
|
||||
],
|
||||
cpp_args : compiler_options + ['-m32', '-Wno-ignored-attributes'],
|
||||
link_args : ['-m32']
|
||||
)
|
||||
endif
|
||||
|
||||
@@ -41,6 +41,10 @@ using InputAdapter = bitsery::InputBufferAdapter<B>;
|
||||
* @param buffer The buffer to write to. This is useful for sending audio and
|
||||
* chunk data since that can vary in size by a lot.
|
||||
*
|
||||
* @warning This operation is not atomic, and calling this function with the
|
||||
* same socket from multiple threads at once will cause issues with the
|
||||
* packets arriving out of order.
|
||||
*
|
||||
* @relates read_object
|
||||
*/
|
||||
template <typename T, typename Socket>
|
||||
|
||||
@@ -17,14 +17,29 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* The name of the wine host name, e.g. `yabridge-host.exe` for the regular 64
|
||||
* bit build.
|
||||
* The name of the Wine VST host application, e.g. `yabridge-host.exe` for the
|
||||
* regular 64-bit build.
|
||||
*/
|
||||
constexpr char yabridge_wine_host_name[] = "@host_binary_64bit@";
|
||||
constexpr char yabridge_individual_host_name[] =
|
||||
"@individual_host_binary_64bit@";
|
||||
|
||||
/**
|
||||
* The name of the 32-bit wine host name, e.g. `yabridge-host-32.exe`.` This is
|
||||
* used as a bitbridge to be able to load legacy 32-bit only Windows plugins
|
||||
* from a 64-bit Linux host.
|
||||
* The name of the group host application, e.g. `yabridge-group.exe` for the
|
||||
* regular 64-bit build.
|
||||
*/
|
||||
constexpr char yabridge_wine_host_name_32bit[] = "@host_binary_32bit@";
|
||||
constexpr char yabridge_group_host_name[] = "@group_host_binary_64bit@";
|
||||
|
||||
/**
|
||||
* The name of the 32-bit Wine VST host application, e.g.
|
||||
* `yabridge-host-32.exe`.` This is used as a bitbridge to be able to load
|
||||
* legacy 32-bit only Windows plugins from a 64-bit Linux host.
|
||||
*/
|
||||
constexpr char yabridge_individual_host_name_32bit[] =
|
||||
"@individual_host_binary_32bit@";
|
||||
|
||||
/**
|
||||
* The name of the 32-bit group host application, e.g. `yabridge-group-32.exe`.`
|
||||
* This is used as a bitbridge to be able to load legacy 32-bit only Windows
|
||||
* plugins from a 64-bit Linux host.
|
||||
*/
|
||||
constexpr char yabridge_group_host_name_32bit[] = "@group_host_binary_32bit@";
|
||||
|
||||
@@ -5,8 +5,10 @@ config_header = configure_file(
|
||||
output : 'config.h',
|
||||
configuration : configuration_data(
|
||||
{
|
||||
'host_binary_32bit': host_name_32bit + '.exe',
|
||||
'host_binary_64bit': host_name_64bit + '.exe',
|
||||
'individual_host_binary_32bit': individual_host_name_32bit + '.exe',
|
||||
'individual_host_binary_64bit': individual_host_name_64bit + '.exe',
|
||||
'group_host_binary_32bit': group_host_name_32bit + '.exe',
|
||||
'group_host_binary_64bit': group_host_name_64bit + '.exe',
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -72,7 +72,12 @@ Logger Logger::create_from_environment(std::string prefix) {
|
||||
if (log_file->is_open()) {
|
||||
return Logger(log_file, verbosity_level, prefix);
|
||||
} else {
|
||||
return Logger(std::shared_ptr<std::ostream>(&std::cerr, [](auto) {}),
|
||||
// For STDERR we sadly can't just use `std::cerr`. In the group process
|
||||
// we need to capture all output generated by the process itself, and
|
||||
// the only way to do this is by reopening the STDERR and STDOUT streams
|
||||
// to a pipe. Luckily `/dev/stderr` stays unaffected, so we can still
|
||||
// write there without causing infinite loops.
|
||||
return Logger(std::make_shared<std::ofstream>("/dev/stderr"),
|
||||
verbosity_level, prefix);
|
||||
}
|
||||
}
|
||||
@@ -301,7 +306,8 @@ bool Logger::should_filter_event(bool is_dispatch, int opcode) {
|
||||
// called tens of times per second
|
||||
// TODO: Figure out what opcode 52 is
|
||||
if ((is_dispatch && (opcode == effEditIdle || opcode == 52)) ||
|
||||
(!is_dispatch && opcode == audioMasterGetTime)) {
|
||||
(!is_dispatch && (opcode == audioMasterGetTime ||
|
||||
opcode == audioMasterGetCurrentProcessLevel))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -108,3 +108,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -571,3 +571,45 @@ struct AudioBuffers {
|
||||
s.value4b(sample_frames);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An object containing the startup options for hosting a plugin in a plugin
|
||||
* group process. These are the exact same options that would have been passed
|
||||
* to `yabridge-host.exe` were the plugin to be hosted individually.
|
||||
*/
|
||||
struct GroupRequest {
|
||||
std::string plugin_path;
|
||||
std::string socket_path;
|
||||
|
||||
bool operator==(const GroupRequest& rhs) const;
|
||||
|
||||
template <typename S>
|
||||
void serialize(S& s) {
|
||||
s.text1b(plugin_path, 4096);
|
||||
s.text1b(socket_path, 4096);
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The response sent back after the group host process receives a `GroupRequest`
|
||||
* object. This only holds the group process's PID because we need to know if
|
||||
* the group process crashes while it is initializing the plugin to prevent us
|
||||
* from waiting indefinitely for the socket to be connected to.
|
||||
*/
|
||||
struct GroupResponse {
|
||||
pid_t pid;
|
||||
|
||||
template <typename S>
|
||||
void serialize(S& s) {
|
||||
s.value4b(pid);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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 "configuration.h"
|
||||
|
||||
#include <fnmatch.h>
|
||||
#include <toml++/toml.h>
|
||||
#include <fstream>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
Configuration::Configuration() {}
|
||||
|
||||
Configuration::Configuration(const fs::path& config_path,
|
||||
const fs::path& yabridge_path)
|
||||
: Configuration() {
|
||||
// Will throw a `toml::parsing_error` if the file cannot be parsed. Better
|
||||
// to throw here rather than failing silently since syntax errors would
|
||||
// otherwise be impossible to spot.
|
||||
toml::table table = toml::parse_file(config_path.string());
|
||||
|
||||
const fs::path relative_path =
|
||||
yabridge_path.lexically_relative(config_path.parent_path());
|
||||
for (const auto& [pattern, value] : table) {
|
||||
// First try to match the glob pattern, allow matching an entire
|
||||
// directory for ease of use. If none of the patterns in the file match
|
||||
// the plugin path then everything will be left at the defaults.
|
||||
if (fnmatch(pattern.c_str(), relative_path.c_str(),
|
||||
FNM_PATHNAME | FNM_LEADING_DIR) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matched_file = config_path;
|
||||
matched_pattern = pattern;
|
||||
|
||||
// If the table is missing some fields then they will simply be left at
|
||||
// their defaults
|
||||
if (toml::table* config = value.as_table(); config != nullptr) {
|
||||
group = (*config)["group"].value<std::string>();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Configuration Configuration::load_for(const fs::path& yabridge_path) {
|
||||
// First find the closest `yabridge.tmol` file for the plugin, falling back
|
||||
// to default configuration settings if it doesn't exist
|
||||
const std::optional<fs::path> config_file =
|
||||
find_dominating_file("yabridge.toml", yabridge_path);
|
||||
if (!config_file.has_value()) {
|
||||
return Configuration();
|
||||
}
|
||||
|
||||
return Configuration(config_file.value(), yabridge_path);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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 <boost/filesystem.hpp>
|
||||
#include <optional>
|
||||
|
||||
/**
|
||||
* An object that's used to provide plugin-specific configuration. Right now
|
||||
* this is only used to declare plugin groups. A plugin group is a set of
|
||||
* plugins that will be hosted in the same process rather than individually so
|
||||
* they can share resources. Configuration file loading works as follows:
|
||||
*
|
||||
* 1. `Configuration::load_for(path)` gets called with a path to the copy of or
|
||||
* symlink to `libyabridge.so` that the plugin host has tried to load.
|
||||
* 2. We start looking for a file named `yabridge.toml` in the same directory as
|
||||
* that `.so` file, iteratively continuing to search one directory higher
|
||||
* until we either find the file or we reach the filesystem root.
|
||||
* 3. If the file is found, then parse it as a TOML file and look for the first
|
||||
* table whose key is a glob pattern that (partially) matches the relative
|
||||
* path between the found `yabridge.toml` and the `.so` file. As a rule of
|
||||
* thumb, if the `find <pattern> -type f` command executed in Bash would list
|
||||
* the `.so` file, then the following table in `yabridge.tmol` would also
|
||||
* match the same `.so` file:
|
||||
*
|
||||
* ```toml
|
||||
* ["<patern>"]
|
||||
* group = "..."
|
||||
* ```
|
||||
* 4. If one of these glob patterns could be matched with the relative path of
|
||||
* the `.so` file then we'll use the settings specified in that section.
|
||||
* Otherwise the default settings will be used.
|
||||
*/
|
||||
class Configuration {
|
||||
public:
|
||||
/**
|
||||
* Create an empty configuration object with default settings.
|
||||
*/
|
||||
Configuration();
|
||||
|
||||
/**
|
||||
* Load the configuration for an instance of yabridge from a configuration
|
||||
* file by matching the plugin's relative path to the glob patterns in that
|
||||
* configuration file. Will leave the object empty if the plugin cannot be
|
||||
* matched to any of the patterns. Not meant to be used directly.
|
||||
*
|
||||
* @throw toml::parsing_error If the file could not be parsed.
|
||||
*
|
||||
* @see Configuration::load_for
|
||||
*/
|
||||
Configuration(const boost::filesystem::path& config_path,
|
||||
const boost::filesystem::path& yabridge_path);
|
||||
|
||||
/**
|
||||
* Load the configuration that belongs to a copy of or symlink to
|
||||
* `libyabridge.so`. If no configuration file could be found then this will
|
||||
* return an empty configuration object with default settings.
|
||||
*
|
||||
* This function will take any optional compile-time features that have not
|
||||
* been enabled into account.
|
||||
*
|
||||
* @param yabridge_path The path to the .so file that's being loaded.by the
|
||||
* VST host. This will be used both for the starting location of the
|
||||
* search and to determine which section in the config file to use.
|
||||
*
|
||||
* @return Either a configuration object populated with values from matched
|
||||
* glob pattern within the found configuration file, or an empty
|
||||
* configuration object if no configuration file could be found or if the
|
||||
* plugin could not be matched to any of the glob patterns in the
|
||||
* configuration file.
|
||||
*/
|
||||
static Configuration load_for(const boost::filesystem::path& yabridge_path);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* be hosted individually instead.
|
||||
*/
|
||||
std::optional<std::string> group;
|
||||
|
||||
/**
|
||||
* The path to the configuration file that was parsed.
|
||||
*/
|
||||
std::optional<boost::filesystem::path> matched_file;
|
||||
|
||||
/**
|
||||
* The matched glob pattern in the above configuration file.
|
||||
*/
|
||||
std::optional<std::string> matched_pattern;
|
||||
};
|
||||
+231
-70
@@ -19,11 +19,8 @@
|
||||
#include <boost/asio/read_until.hpp>
|
||||
#include <boost/process/env.hpp>
|
||||
#include <boost/process/io.hpp>
|
||||
#include <iostream>
|
||||
|
||||
#ifdef USE_WINEDBG
|
||||
#include <boost/process/start_dir.hpp>
|
||||
#endif
|
||||
#include <iostream>
|
||||
|
||||
// Generated inside of build directory
|
||||
#include <src/common/config/config.h>
|
||||
@@ -52,16 +49,21 @@ PluginBridge& get_bridge_instance(const AEffect& plugin) {
|
||||
return *static_cast<PluginBridge*>(plugin.ptr3);
|
||||
}
|
||||
|
||||
// TODO: It would be nice to have a better way to encapsulate the small
|
||||
// differences in behavior when using plugin groups, i.e. everywhere where
|
||||
// we check for `config.group.has_value()`
|
||||
|
||||
PluginBridge::PluginBridge(audioMasterCallback host_callback)
|
||||
: vst_plugin_path(find_vst_plugin()),
|
||||
: config(Configuration::load_for(get_this_file_location())),
|
||||
vst_plugin_path(find_vst_plugin()),
|
||||
vst_plugin_arch(find_vst_architecture(vst_plugin_path)),
|
||||
vst_host_path(find_vst_host(vst_plugin_arch)),
|
||||
vst_host_path(find_vst_host(vst_plugin_arch, config.group.has_value())),
|
||||
// All the fields should be zero initialized because
|
||||
// `Vst2PluginInstance::vstAudioMasterCallback` from Bitwig's plugin
|
||||
// bridge will crash otherwise
|
||||
plugin(),
|
||||
io_context(),
|
||||
socket_endpoint(generate_endpoint_name().string()),
|
||||
socket_endpoint(generate_plugin_endpoint().string()),
|
||||
socket_acceptor(io_context, socket_endpoint),
|
||||
host_vst_dispatch(io_context),
|
||||
host_vst_dispatch_midi_events(io_context),
|
||||
@@ -73,58 +75,9 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
|
||||
create_logger_prefix(socket_endpoint.path()))),
|
||||
wine_version(get_wine_version()),
|
||||
wine_stdout(io_context),
|
||||
wine_stderr(io_context),
|
||||
#ifndef USE_WINEDBG
|
||||
vst_host(vst_host_path,
|
||||
// The Wine VST host needs to know which plugin to load
|
||||
// and which Unix domain socket to connect to
|
||||
vst_plugin_path,
|
||||
socket_endpoint.path(),
|
||||
bp::env = set_wineprefix(),
|
||||
bp::std_out = wine_stdout,
|
||||
bp::std_err = wine_stderr)
|
||||
#else
|
||||
// This is set up for KDE Plasma. Other desktop environments and window
|
||||
// managers require some slight modifications to spawn a detached terminal
|
||||
// emulator.
|
||||
vst_host("/usr/bin/kstart5",
|
||||
"konsole",
|
||||
"--",
|
||||
"-e",
|
||||
"winedbg",
|
||||
"--gdb",
|
||||
vst_host_path.string() + ".so",
|
||||
vst_plugin_path.filename(),
|
||||
socket_endpoint.path(),
|
||||
bp::env = set_wineprefix(),
|
||||
// winedbg has no reliable way to escape spaces, so we'll start
|
||||
// the process in the plugin's directory
|
||||
bp::start_dir = vst_plugin_path.parent_path())
|
||||
#endif
|
||||
{
|
||||
logger.log("Initializing yabridge version " +
|
||||
std::string(yabridge_git_version));
|
||||
logger.log("host: '" + vst_host_path.string() + "'");
|
||||
logger.log("plugin: '" + vst_plugin_path.string() + "'");
|
||||
logger.log("socket: '" + socket_endpoint.path() + "'");
|
||||
logger.log("wine prefix: '" +
|
||||
find_wineprefix().value_or("<default>").string() + "'");
|
||||
logger.log("wine version: '" + wine_version + "'");
|
||||
|
||||
// Include a list of enabled compile-tiem features, mostly to make debug
|
||||
// logs more useful
|
||||
logger.log("");
|
||||
logger.log("Enabled features:");
|
||||
#ifdef USE_BITBRIDGE
|
||||
logger.log("- bitbridge support");
|
||||
#endif
|
||||
#ifdef USE_WINEDBG
|
||||
logger.log("- winedbg");
|
||||
#endif
|
||||
#if !(defined(USE_BITBRIDGE) || defined(USE_WINEDBG))
|
||||
logger.log(" <none>");
|
||||
#endif
|
||||
logger.log("");
|
||||
wine_stderr(io_context) {
|
||||
log_init_message();
|
||||
launch_vst_host();
|
||||
|
||||
// Print the Wine host's STDOUT and STDERR streams to the log file. This
|
||||
// should be done before trying to accept the sockets as otherwise we will
|
||||
@@ -146,10 +99,27 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
|
||||
if (finished_accepting_sockets) {
|
||||
return;
|
||||
}
|
||||
if (!vst_host.running()) {
|
||||
throw std::runtime_error(
|
||||
"The Wine process failed to start. Check the output above "
|
||||
"for more information.");
|
||||
|
||||
// When using regular individually hosted plugins we can simply
|
||||
// check whether the process is still running, but Boost.Process
|
||||
// does not allow you to do the same thing for a process that's not
|
||||
// a child if this process. When using plugin groups we'll have to
|
||||
// manually check whether the PID returned by the group host process
|
||||
// is still active.
|
||||
if (config.group.has_value()) {
|
||||
if (kill(vst_host_pid, 0) != 0) {
|
||||
logger.log(
|
||||
"The group host process has exited unexpectedly. Check "
|
||||
"the output above for more information.");
|
||||
std::terminate();
|
||||
}
|
||||
} else {
|
||||
if (!vst_host.running()) {
|
||||
logger.log(
|
||||
"The Wine process failed to start. Check the output "
|
||||
"above for more information.");
|
||||
std::terminate();
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(1s);
|
||||
@@ -186,7 +156,7 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
|
||||
try {
|
||||
while (true) {
|
||||
// TODO: Think of a nicer way to structure this and the similar
|
||||
// handler in `WineBridge::handle_dispatch_midi_events`
|
||||
// handler in `Vst2Bridge::handle_dispatch_midi_events`
|
||||
receive_event(
|
||||
vst_host_callback, std::pair<Logger&, bool>(logger, false),
|
||||
[&](Event& event) {
|
||||
@@ -494,7 +464,16 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
|
||||
// loaded into the Wine process crashed during shutdown
|
||||
logger.log("The plugin crashed during shutdown, ignoring");
|
||||
}
|
||||
vst_host.terminate();
|
||||
|
||||
// Don't terminate group host processes. They will shut down
|
||||
// automatically after all plugins have exited.
|
||||
if (!config.group.has_value()) {
|
||||
vst_host.terminate();
|
||||
} else {
|
||||
// Manually the dispatch socket will cause the host process to
|
||||
// terminate
|
||||
host_vst_dispatch.close();
|
||||
}
|
||||
|
||||
// The `stop()` method will cause the IO context to just drop all of
|
||||
// its work immediately and not throw any exceptions that would have
|
||||
@@ -503,6 +482,10 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
|
||||
|
||||
// These threads should now be finished because we've forcefully
|
||||
// terminated the Wine process, interupting their socket operations
|
||||
if (group_host_connect_handler.joinable()) {
|
||||
// This thread is only used when using plugin groups
|
||||
group_host_connect_handler.join();
|
||||
}
|
||||
host_callback_handler.join();
|
||||
wine_io_handler.join();
|
||||
|
||||
@@ -633,17 +616,195 @@ void PluginBridge::async_log_pipe_lines(patched_async_pipe& pipe,
|
||||
boost::asio::streambuf& buffer,
|
||||
std::string prefix) {
|
||||
boost::asio::async_read_until(
|
||||
pipe, buffer, '\n', [&, prefix](const auto&, size_t) {
|
||||
pipe, buffer, '\n',
|
||||
[&, prefix](const boost::system::error_code& error, size_t) {
|
||||
// When we get an error code then that likely means that the pipe
|
||||
// has been clsoed and we have reached the end of the file
|
||||
if (error.failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
std::getline(std::istream(&buffer), line);
|
||||
logger.log(prefix + line);
|
||||
|
||||
// Not sure why, but this async read will keep reading a ton of
|
||||
// empty lines after the Wine process crashes
|
||||
if (vst_host.running()) {
|
||||
async_log_pipe_lines(pipe, buffer, prefix);
|
||||
async_log_pipe_lines(pipe, buffer, prefix);
|
||||
});
|
||||
}
|
||||
|
||||
void PluginBridge::launch_vst_host() {
|
||||
const bp::environment host_env = set_wineprefix();
|
||||
|
||||
#ifndef USE_WINEDBG
|
||||
const std::vector<std::string> host_command{vst_host_path.string()};
|
||||
#else
|
||||
// This is set up for KDE Plasma. Other desktop environments and window
|
||||
// managers require some slight modifications to spawn a detached terminal
|
||||
// emulator.
|
||||
const std::vector<std::string> host_command{"/usr/bin/kstart5",
|
||||
"konsole",
|
||||
"--",
|
||||
"-e",
|
||||
"winedbg",
|
||||
"--gdb",
|
||||
vst_host_path.string() + ".so"};
|
||||
#endif
|
||||
|
||||
#ifndef USE_WINEDBG
|
||||
const fs::path plugin_path = vst_plugin_path;
|
||||
const fs::path starting_dir = fs::current_path();
|
||||
#else
|
||||
// winedbg has no reliable way to escape spaces, so we'll start the process
|
||||
// in the plugin's directory
|
||||
const fs::path plugin_path = vst_plugin_path.filename();
|
||||
const fs::path starting_dir = vst_plugin_path.parent_path();
|
||||
|
||||
if (plugin_path.string().find(' ') != std::string::npos) {
|
||||
logger.log("Warning: winedbg does not support paths containing spaces");
|
||||
}
|
||||
#endif
|
||||
const fs::path socket_path = socket_endpoint.path();
|
||||
|
||||
if (!config.group.has_value()) {
|
||||
vst_host =
|
||||
bp::child(host_command, plugin_path, socket_path,
|
||||
bp::env = host_env, bp::std_out = wine_stdout,
|
||||
bp::std_err = wine_stderr, bp::start_dir = starting_dir);
|
||||
return;
|
||||
}
|
||||
|
||||
// When using plugin groups, we'll first try to connect to an existing group
|
||||
// host process and ask it to host our plugin. If no such process exists,
|
||||
// then we'll start a new process. In the event that two yabridge instances
|
||||
// simultaneously try to start a new group process for the same group, then
|
||||
// the last process to connect to the socket will terminate gracefully and
|
||||
// the first process will handle the connections for both yabridge
|
||||
// instances.
|
||||
fs::path wine_prefix = host_env.at("WINEPREFIX").to_string();
|
||||
if (host_env.at("WINEPREFIX").empty()) {
|
||||
// Fall back to `~/.wine` if this has not been set or detected. This
|
||||
// would happen if the plugin's .dll file is not inside of a Wine
|
||||
// prefix. If this happens, then the Wine instance will be launched in
|
||||
// the default Wine prefix, so we should reflect that here.
|
||||
wine_prefix = fs::path(host_env.at("HOME").to_string()) / ".wine";
|
||||
}
|
||||
|
||||
const fs::path group_socket_path = generate_group_endpoint(
|
||||
config.group.value(), wine_prefix, vst_plugin_arch);
|
||||
|
||||
try {
|
||||
// Request the existing group host process to host our plugin, and store
|
||||
// the PID of that process so we'll know if it has crashed
|
||||
boost::asio::local::stream_protocol::socket group_socket(io_context);
|
||||
group_socket.connect(group_socket_path.string());
|
||||
|
||||
write_object(group_socket,
|
||||
GroupRequest{plugin_path.string(), socket_path.string()});
|
||||
const auto response = read_object<GroupResponse>(group_socket);
|
||||
|
||||
vst_host_pid = response.pid;
|
||||
} catch (const boost::system::system_error&) {
|
||||
// In case we could not connect to the socket, then we'll start a
|
||||
// new group host process. This process is detached immediately
|
||||
// because it should run independently of this yabridge instance as
|
||||
// it will likely outlive it.
|
||||
vst_host =
|
||||
bp::child(host_command, group_socket_path, bp::env = host_env,
|
||||
bp::std_out = wine_stdout, bp::std_err = wine_stderr,
|
||||
bp::start_dir = starting_dir);
|
||||
vst_host_pid = vst_host.id();
|
||||
vst_host.detach();
|
||||
|
||||
// We now want to connect to the socket the in the exact same way as
|
||||
// above. The only problem is that it may take some time for the
|
||||
// process to start depending on Wine's current state. We'll defer
|
||||
// this to a thread so we can finish the rest of the startup in the
|
||||
// meantime.
|
||||
group_host_connect_handler = std::thread([&, group_socket_path,
|
||||
plugin_path, socket_path]() {
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
// TODO: Replace this polling with inotify when encapsulating
|
||||
// the different host launch behaviors
|
||||
while (vst_host.running()) {
|
||||
std::this_thread::sleep_for(20ms);
|
||||
|
||||
try {
|
||||
// This is the exact same connection sequence as above
|
||||
boost::asio::local::stream_protocol::socket group_socket(
|
||||
io_context);
|
||||
group_socket.connect(group_socket_path.string());
|
||||
|
||||
write_object(group_socket,
|
||||
GroupRequest{plugin_path.string(),
|
||||
socket_path.string()});
|
||||
const auto response =
|
||||
read_object<GroupResponse>(group_socket);
|
||||
|
||||
// If two group processes started at the same time, than the
|
||||
// first one will be the one to respond to the host request
|
||||
vst_host_pid = response.pid;
|
||||
return;
|
||||
} catch (const boost::system::system_error&) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PluginBridge::log_init_message() {
|
||||
std::stringstream init_msg;
|
||||
|
||||
init_msg << "Initializing yabridge version " << yabridge_git_version
|
||||
<< std::endl;
|
||||
init_msg << "host: '" << vst_host_path.string() << "'" << std::endl;
|
||||
init_msg << "plugin: '" << vst_plugin_path.string() << "'"
|
||||
<< std::endl;
|
||||
init_msg << "socket: '" << socket_endpoint.path() << "'" << std::endl;
|
||||
init_msg << "wine prefix: '"
|
||||
<< find_wineprefix().value_or("<default>").string() << "'"
|
||||
<< std::endl;
|
||||
init_msg << "wine version: '" << wine_version << "'" << std::endl;
|
||||
init_msg << std::endl;
|
||||
|
||||
// Print the path to the currently loaded configuration file and all
|
||||
// settings in use. Printing the matched glob pattern could also be useful
|
||||
// but it'll be very noisy and it's likely going to be clear from the shown
|
||||
// values anyways.
|
||||
init_msg << "config from: '"
|
||||
<< config.matched_file.value_or("<defaults>").string() << "'"
|
||||
<< std::endl;
|
||||
init_msg << "hosting mode: '";
|
||||
if (config.group.has_value()) {
|
||||
init_msg << "plugin group \"" << config.group.value() << "\"";
|
||||
} else {
|
||||
init_msg << "individually";
|
||||
}
|
||||
if (vst_plugin_arch == PluginArchitecture::vst_32) {
|
||||
init_msg << ", 32-bit";
|
||||
} else {
|
||||
init_msg << ", 64-bit";
|
||||
}
|
||||
init_msg << "'" << std::endl;
|
||||
init_msg << std::endl;
|
||||
|
||||
// Include a list of enabled compile-tiem features, mostly to make debug
|
||||
// logs more useful
|
||||
init_msg << "Enabled features:" << std::endl;
|
||||
#ifdef USE_BITBRIDGE
|
||||
init_msg << "- bitbridge support" << std::endl;
|
||||
#endif
|
||||
#ifdef USE_WINEDBG
|
||||
init_msg << "- winedbg" << std::endl;
|
||||
#endif
|
||||
#if !(defined(USE_BITBRIDGE) || defined(USE_WINEDBG))
|
||||
init_msg << " <none>" << std::endl;
|
||||
#endif
|
||||
init_msg << std::endl;
|
||||
|
||||
for (std::string line = ""; std::getline(init_msg, line);) {
|
||||
logger.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
// The below functions are proxy functions for the methods defined in
|
||||
|
||||
+70
-14
@@ -26,6 +26,7 @@
|
||||
#include <thread>
|
||||
|
||||
#include "../common/logging.h"
|
||||
#include "configuration.h"
|
||||
#include "utils.h"
|
||||
|
||||
/**
|
||||
@@ -64,7 +65,7 @@ class PluginBridge {
|
||||
* Ask the VST plugin to process audio for us. If the plugin somehow does
|
||||
* not support `processReplacing()` and only supports the old `process()`
|
||||
* function, then this will be handled implicitely in
|
||||
* `WineBridge::handle_process_replacing()`.
|
||||
* `Vst2Bridge::handle_process_replacing()`.
|
||||
*/
|
||||
void process_replacing(AEffect* plugin,
|
||||
float** inputs,
|
||||
@@ -73,6 +74,14 @@ class PluginBridge {
|
||||
float get_parameter(AEffect* plugin, int index);
|
||||
void set_parameter(AEffect* plugin, int index, float value);
|
||||
|
||||
/**
|
||||
* The configuration for this instance of yabridge. Set based on the values
|
||||
* from a `yabridge.toml`, if it exists.
|
||||
*
|
||||
* @see Configuration::load_for
|
||||
*/
|
||||
Configuration config;
|
||||
|
||||
/**
|
||||
* The path to the .dll being loaded in the Wine VST host.
|
||||
*/
|
||||
@@ -95,19 +104,6 @@ class PluginBridge {
|
||||
*/
|
||||
AEffect plugin;
|
||||
|
||||
/**
|
||||
* The VST host can query a plugin for arbitrary binary data such as
|
||||
* presets. It will expect the plugin to write back a pointer that points to
|
||||
* that data. This vector is where we store the chunk data for the last
|
||||
* `effGetChunk` event.
|
||||
*/
|
||||
std::vector<uint8_t> chunk_data;
|
||||
/**
|
||||
* The VST host will expect to be returned a pointer to a struct that stores
|
||||
* the dimensions of the editor window.
|
||||
*/
|
||||
VstRect editor_rectangle;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Write output from an async pipe to the log on a line by line basis.
|
||||
@@ -121,6 +117,24 @@ class PluginBridge {
|
||||
boost::asio::streambuf& buffer,
|
||||
std::string prefix = "");
|
||||
|
||||
/**
|
||||
* Launch the Wine VST host to host the plugin. When using plugin groups,
|
||||
* this will first try to connect to the plugin group's socket (determined
|
||||
* based on group name, Wine prefix and architecture). If that fails, it
|
||||
* will launch a new, detached group host process. This will likely outlive
|
||||
* this plugin instance if multiple instances of yabridge using the same
|
||||
* plugin group are in use. In the event that two yabridge instances are
|
||||
* initialized at the same time and both instances spawn their own group
|
||||
* host process, then the later one will simply terminate gracefully after
|
||||
* it fails to listen on the socket.
|
||||
*/
|
||||
void launch_vst_host();
|
||||
|
||||
/**
|
||||
* Format and log all relevant debug information during initialization.
|
||||
*/
|
||||
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;
|
||||
@@ -184,6 +198,12 @@ class PluginBridge {
|
||||
*/
|
||||
audioMasterCallback host_callback_function;
|
||||
|
||||
/**
|
||||
* The logging facility used for this instance of yabridge. See
|
||||
* `Logger::create_from_env()` for how this is configured.
|
||||
*
|
||||
* @see Logger::create_from_env
|
||||
*/
|
||||
Logger logger;
|
||||
|
||||
/**
|
||||
@@ -210,8 +230,31 @@ class PluginBridge {
|
||||
|
||||
/**
|
||||
* The Wine process hosting the Windows VST plugin.
|
||||
*
|
||||
* @see launch_vst_host
|
||||
*/
|
||||
boost::process::child vst_host;
|
||||
/**
|
||||
* The PID of the vst host process. Needed for checking whether the group
|
||||
* host is still active if we are connecting to an already running group
|
||||
* host instance.
|
||||
*
|
||||
* TODO: Remove this after encapsulating the minor differences in individual
|
||||
* and group host handling
|
||||
*/
|
||||
pid_t vst_host_pid;
|
||||
/**
|
||||
* A thread that waits for the group host to have started and then ask it to
|
||||
* host our plugin. This is used to defer the request since it may take a
|
||||
* little while until the group host process is up and running. This way we
|
||||
* don't have to delay the rest of the initialization process.
|
||||
*
|
||||
* TODO: Remove this after encapsulating the minor differences in individual
|
||||
* and group host handling
|
||||
* TODO: Replace this with inotify to prevent delays and to reduce wasting
|
||||
* resources
|
||||
*/
|
||||
std::thread group_host_connect_handler;
|
||||
|
||||
/**
|
||||
* A scratch buffer for sending and receiving data during `process` and
|
||||
@@ -219,6 +262,19 @@ class PluginBridge {
|
||||
*/
|
||||
std::vector<uint8_t> process_buffer;
|
||||
|
||||
/**
|
||||
* The VST host can query a plugin for arbitrary binary data such as
|
||||
* presets. It will expect the plugin to write back a pointer that points to
|
||||
* that data. This vector is where we store the chunk data for the last
|
||||
* `effGetChunk` event.
|
||||
*/
|
||||
std::vector<uint8_t> chunk_data;
|
||||
/**
|
||||
* The VST host will expect to be returned a pointer to a struct that stores
|
||||
* the dimensions of the editor window.
|
||||
*/
|
||||
VstRect editor_rectangle;
|
||||
|
||||
/**
|
||||
* Sending MIDI events sent to the host by the plugin using
|
||||
* `audioMasterProcessEvents` function has to be done during the processing
|
||||
|
||||
+43
-18
@@ -28,6 +28,8 @@
|
||||
// Generated inside of build directory
|
||||
#include <src/common/config/config.h>
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
namespace bp = boost::process;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
@@ -53,19 +55,13 @@ std::string create_logger_prefix(const fs::path& socket_path) {
|
||||
}
|
||||
|
||||
std::optional<fs::path> find_wineprefix() {
|
||||
// Try to locate the Wine prefix the plugin's .dll file is located in by
|
||||
// finding the first parent directory that contains a directory named
|
||||
// `dosdevices`
|
||||
fs::path wineprefix_path = find_vst_plugin();
|
||||
while (wineprefix_path != "") {
|
||||
if (fs::is_directory(wineprefix_path / "dosdevices")) {
|
||||
return wineprefix_path;
|
||||
}
|
||||
|
||||
wineprefix_path = wineprefix_path.parent_path();
|
||||
std::optional<fs::path> dosdevices_dir =
|
||||
find_dominating_file("dosdevices", find_vst_plugin(), fs::is_directory);
|
||||
if (!dosdevices_dir.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
return dosdevices_dir->parent_path();
|
||||
}
|
||||
|
||||
PluginArchitecture find_vst_architecture(fs::path plugin_path) {
|
||||
@@ -115,10 +111,12 @@ PluginArchitecture find_vst_architecture(fs::path plugin_path) {
|
||||
throw std::runtime_error(error_msg.str());
|
||||
}
|
||||
|
||||
fs::path find_vst_host(PluginArchitecture plugin_arch) {
|
||||
auto host_name = yabridge_wine_host_name;
|
||||
fs::path find_vst_host(PluginArchitecture plugin_arch, bool use_plugin_groups) {
|
||||
auto host_name = use_plugin_groups ? yabridge_group_host_name
|
||||
: yabridge_individual_host_name;
|
||||
if (plugin_arch == PluginArchitecture::vst_32) {
|
||||
host_name = yabridge_wine_host_name_32bit;
|
||||
host_name = use_plugin_groups ? yabridge_group_host_name_32bit
|
||||
: yabridge_individual_host_name_32bit;
|
||||
}
|
||||
|
||||
fs::path host_path =
|
||||
@@ -166,7 +164,27 @@ fs::path find_vst_plugin() {
|
||||
"VST plugin .dll file.");
|
||||
}
|
||||
|
||||
fs::path generate_endpoint_name() {
|
||||
boost::filesystem::path generate_group_endpoint(
|
||||
std::string group_name,
|
||||
boost::filesystem::path wine_prefix,
|
||||
PluginArchitecture architecture) {
|
||||
std::ostringstream socket_name;
|
||||
socket_name << "yabridge-group-" << group_name << "-"
|
||||
<< std::hash<std::string>{}(wine_prefix.string()) << "-";
|
||||
switch (architecture) {
|
||||
case PluginArchitecture::vst_32:
|
||||
socket_name << "x32";
|
||||
break;
|
||||
case PluginArchitecture::vst_64:
|
||||
socket_name << "x64";
|
||||
break;
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -204,9 +222,16 @@ fs::path get_this_file_location() {
|
||||
// on both Ubuntu 18.04 and 20.04, but not on Arch based distros.
|
||||
// Under Linux a path starting with two slashes is treated the same as
|
||||
// a path starting with only a single slash, but Wine will refuse to
|
||||
// load any files when the path starts with two slashes. Prepending
|
||||
// `/` to a pad coerces theses two slashes into a single slash.
|
||||
return "/" / boost::dll::this_line_location();
|
||||
// load any files when the path starts with two slashes. The easiest
|
||||
// way to work around this if this happens is to just add another
|
||||
// leading slash and then normalize the path, since three or more
|
||||
// slashes will be coerced into a single slash.
|
||||
fs::path this_file = boost::dll::this_line_location();
|
||||
if (this_file.string().find("//") == 0) {
|
||||
this_file = ("/" / this_file).lexically_normal();
|
||||
}
|
||||
|
||||
return this_file;
|
||||
}
|
||||
|
||||
std::string get_wine_version() {
|
||||
|
||||
+65
-3
@@ -80,11 +80,14 @@ PluginArchitecture find_vst_architecture(boost::filesystem::path);
|
||||
*
|
||||
* @param plugin_arch The architecture of the plugin, either 64-bit or 32-bit.
|
||||
* Used to determine which host application to use, if available.
|
||||
* @param use_plugin_groups Whether the plugin is using plugin groups and we
|
||||
* should be looking for the group host instead of the individual plugin host.
|
||||
*
|
||||
* @return The a path to the VST host, if found.
|
||||
* @throw std::runtime_error If the Wine VST host could not be found.
|
||||
*/
|
||||
boost::filesystem::path find_vst_host(PluginArchitecture plugin_arch);
|
||||
boost::filesystem::path find_vst_host(PluginArchitecture plugin_arch,
|
||||
bool use_plugin_groups);
|
||||
|
||||
/**
|
||||
* Find the VST plugin .dll file that corresponds to this copy of
|
||||
@@ -101,13 +104,40 @@ boost::filesystem::path find_vst_plugin();
|
||||
|
||||
/**
|
||||
* Locate the Wine prefix this file is located in, if it is inside of a wine
|
||||
* prefix.
|
||||
* prefix. This is done by locating the first parent directory that contains a
|
||||
* directory named `dosdevices`.
|
||||
*
|
||||
* @return Either the path to the Wine prefix (containing the `drive_c?`
|
||||
* directory), or `std::nullopt` if it is not inside of a wine prefix.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param group_name The name of the plugin group.
|
||||
* @param wine_prefix The name of the Wine prefix in use. This should be
|
||||
* obtained by first calling `set_wineprefix()` to allow the user to override
|
||||
* this, and then falling back to `$HOME/.wine` if the environment variable is
|
||||
* unset. Otherwise plugins run from outwide of a Wine prefix will not be
|
||||
* groupable with those run from within `~/.wine` even though they both run
|
||||
* under the same prefix.
|
||||
* @param architecture The architecture the plugin is using, since 64-bit
|
||||
* processes can't host 32-bit plugins and the other way around.
|
||||
*
|
||||
* @return A socket endpoint path that corresponds to the format described
|
||||
* above.
|
||||
*/
|
||||
boost::filesystem::path generate_group_endpoint(
|
||||
std::string group_name,
|
||||
boost::filesystem::path wine_prefix,
|
||||
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
|
||||
@@ -116,7 +146,7 @@ std::optional<boost::filesystem::path> find_wineprefix();
|
||||
* @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_endpoint_name();
|
||||
boost::filesystem::path generate_plugin_endpoint();
|
||||
|
||||
/**
|
||||
* Return a path to this `.so` file. This can be used to find out from where
|
||||
@@ -140,3 +170,35 @@ std::string get_wine_version();
|
||||
* using the user's default prefix.
|
||||
*/
|
||||
boost::process::environment set_wineprefix();
|
||||
|
||||
/**
|
||||
* Starting from the starting file or directory, go up in the directory
|
||||
* hierarchy until we find a file named `filename`.
|
||||
*
|
||||
* @param filename The name of the file we're looking for. This can also be a
|
||||
* directory name since directories are also files.
|
||||
* @param starting_from The directory to start searching in. If this is a file,
|
||||
* then start searching in the directory the file is located in.
|
||||
* @param predicate The predicate to use to check if the path matches a file.
|
||||
* Needed as an easy way to limit the search to directories only since C++17
|
||||
* does not have any built in coroutines or generators.
|
||||
*
|
||||
* @return The path to the *file* found, or `std::nullopt` if the file could not
|
||||
* be found.
|
||||
*/
|
||||
template <typename F = bool(const boost::filesystem::path&)>
|
||||
std::optional<boost::filesystem::path> find_dominating_file(
|
||||
const std::string& filename,
|
||||
boost::filesystem::path starting_dir,
|
||||
F predicate = boost::filesystem::exists) {
|
||||
while (starting_dir != "") {
|
||||
const boost::filesystem::path candidate = starting_dir / filename;
|
||||
if (predicate(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
starting_dir = starting_dir.parent_path();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
// 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 "group.h"
|
||||
|
||||
#include <unistd.h>
|
||||
#include <boost/asio/read_until.hpp>
|
||||
#include <boost/process/environment.hpp>
|
||||
#include <regex>
|
||||
|
||||
#include "../../common/communication.h"
|
||||
|
||||
// FIXME: `std::filesystem` is broken in wineg++, at least under Wine 5.8. Any
|
||||
// path operation will thrown an encoding related error
|
||||
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:
|
||||
*
|
||||
* 1. The endpoint does not already exist, and we can simply create an endpoint.
|
||||
* 2. The endpoint already exists but it is stale and no process is currently
|
||||
* listening. In this case we can remove the file and start listening.
|
||||
* 3. The endpoint already exists and another process is currently listening on
|
||||
* it. In this situation we will throw immediately and we'll terminate this
|
||||
* process.
|
||||
*
|
||||
* If anyone knows a better way to handle this, please let me know!
|
||||
*
|
||||
* @throw std::runtime_error If another process is already listening on the
|
||||
* endpoint.
|
||||
*/
|
||||
boost::asio::local::stream_protocol::acceptor create_acceptor_if_inactive(
|
||||
boost::asio::io_context& io_context,
|
||||
boost::asio::local::stream_protocol::endpoint& endpoint);
|
||||
|
||||
/**
|
||||
* Create a logger prefix containing the group name based on the socket path.
|
||||
*/
|
||||
std::string create_logger_prefix(const fs::path& socket_path);
|
||||
|
||||
uint32_t WINAPI handle_plugin_dispatch_proxy(void* param);
|
||||
|
||||
/**
|
||||
* CreateThread() is great and allows you to pass a single value to the
|
||||
* function, so we'll use this to pass both `this` and the parameters to the
|
||||
* below thread function so it can do its thing.
|
||||
*
|
||||
* @relates handle_plugin_dispatch_proxy
|
||||
*/
|
||||
using handle_plugin_dispatch_parameters = std::pair<GroupBridge*, GroupRequest>;
|
||||
|
||||
StdIoCapture::StdIoCapture(boost::asio::io_context& io_context,
|
||||
int file_descriptor)
|
||||
: pipe(io_context),
|
||||
target_fd(file_descriptor),
|
||||
original_fd_copy(dup(file_descriptor)) {
|
||||
// We'll use the second element of these two file descriptors to reopen
|
||||
// `file_descriptor`, and the first one to read the captured contents from
|
||||
::pipe(pipe_fd);
|
||||
|
||||
// We've already created a copy of the original file descriptor, so we can
|
||||
// reopen it using the newly created pipe
|
||||
dup2(pipe_fd[1], target_fd);
|
||||
close(pipe_fd[1]);
|
||||
|
||||
pipe.assign(pipe_fd[0]);
|
||||
}
|
||||
|
||||
StdIoCapture::~StdIoCapture() {
|
||||
// Restore the original file descriptor and close the pipe. The other wend
|
||||
// was already closed in the constructor.
|
||||
dup2(original_fd_copy, target_fd);
|
||||
close(original_fd_copy);
|
||||
close(pipe_fd[0]);
|
||||
}
|
||||
|
||||
GroupBridge::GroupBridge(boost::filesystem::path group_socket_path)
|
||||
: logger(Logger::create_from_environment(
|
||||
create_logger_prefix(group_socket_path))),
|
||||
plugin_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) {
|
||||
// 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::thread([&]() { stdio_context.run(); });
|
||||
}
|
||||
|
||||
GroupBridge::~GroupBridge() {
|
||||
stdio_context.stop();
|
||||
stdio_handler.join();
|
||||
}
|
||||
|
||||
void GroupBridge::handle_plugin_dispatch(const GroupRequest request) {
|
||||
// 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);
|
||||
|
||||
// Blocks this thread until the plugin shuts down, handling all events on
|
||||
// the main IO context
|
||||
bridge->handle_dispatch_multi(plugin_context);
|
||||
logger.log("'" + request.plugin_path + "' has exited");
|
||||
|
||||
// After the plugin has exited, we'll remove this thread's plugin from the
|
||||
// active plugins. If no active plugins remain, then we'll terminate the
|
||||
// process.
|
||||
std::lock_guard lock(active_plugins_mutex);
|
||||
active_plugins.erase(request);
|
||||
|
||||
// Defer actually shutting down the process to allow for fast plugin
|
||||
// scanning by allowing plugins to reuse the same group host process
|
||||
shutdown_timer.expires_after(2s);
|
||||
shutdown_timer.async_wait([&](const boost::system::error_code& error) {
|
||||
// A previous timer gets canceled automatically when another plugin
|
||||
// exits
|
||||
if (error.failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(active_plugins_mutex);
|
||||
if (active_plugins.size() == 0) {
|
||||
logger.log(
|
||||
"All plugins have exited, shutting down the group process");
|
||||
plugin_context.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void GroupBridge::handle_incoming_connections() {
|
||||
accept_requests();
|
||||
async_handle_events();
|
||||
|
||||
logger.log(
|
||||
"Group host is up and running, now accepting incoming connections");
|
||||
plugin_context.run();
|
||||
}
|
||||
|
||||
bool GroupBridge::should_skip_message_loop() {
|
||||
// We do not need additional locking since the call to `AEffect::dispatcher`
|
||||
// and the actual event handling and message loop handling are performed
|
||||
// within the IO context and these values thus can't change while another
|
||||
// the message loop is being running
|
||||
std::lock_guard lock(active_plugins_mutex);
|
||||
for (auto& [parameters, value] : active_plugins) {
|
||||
auto& [thread, bridge] = value;
|
||||
if (bridge->should_skip_message_loop()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void GroupBridge::accept_requests() {
|
||||
group_socket_acceptor.async_accept(
|
||||
[&](const boost::system::error_code& error,
|
||||
boost::asio::local::stream_protocol::socket socket) {
|
||||
std::lock_guard lock(active_plugins_mutex);
|
||||
|
||||
// Stop the whole process when the socket gets closed unexpectedly
|
||||
if (error.failed()) {
|
||||
logger.log("Error while listening for incoming connections:");
|
||||
logger.log(error.message());
|
||||
|
||||
plugin_context.stop();
|
||||
}
|
||||
|
||||
// Read the parameters, and then host the plugin in this process
|
||||
// just like if we would be hosting the plugin individually through
|
||||
// `yabridge-hsot.exe`. We will reply with this process's PID so the
|
||||
// yabridge plugin will be able to tell if the plugin has caused
|
||||
// this process to crash during its initialization to prevent
|
||||
// waiting indefinitely on the sockets to be connected to.
|
||||
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.find(request) == active_plugins.end());
|
||||
|
||||
// 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 + "'");
|
||||
try {
|
||||
auto bridge = std::make_unique<Vst2Bridge>(request.plugin_path,
|
||||
request.socket_path);
|
||||
logger.log("Finished initializing '" + request.plugin_path +
|
||||
"'");
|
||||
|
||||
// CreateThread() doesn't support multiple arguments and
|
||||
// requires manualy memory management.
|
||||
handle_plugin_dispatch_parameters* thread_params =
|
||||
new std::pair<GroupBridge*, GroupRequest>(this, request);
|
||||
active_plugins[request] = std::pair(
|
||||
Win32Thread(handle_plugin_dispatch_proxy, thread_params),
|
||||
std::move(bridge));
|
||||
} catch (const std::runtime_error& error) {
|
||||
logger.log("Error while initializing '" + request.plugin_path +
|
||||
"':");
|
||||
logger.log(error.what());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
{
|
||||
// Always handle X11 events
|
||||
std::lock_guard lock(active_plugins_mutex);
|
||||
for (auto& [parameters, value] : active_plugins) {
|
||||
auto& [thread, bridge] = value;
|
||||
bridge->handle_x11_events();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Win32 messages unless plugins are in the middle of opening
|
||||
// their editor
|
||||
// TODO: Check if those same weird crashes with Serum are happening
|
||||
// again with these normal threads
|
||||
if (!should_skip_message_loop()) {
|
||||
MSG msg;
|
||||
|
||||
// Keep the loop responsive by not handling too many events at once
|
||||
// TODO: For some reason the Melda plugins run into a seemingly
|
||||
// infinite timer loop for a little while after opening a
|
||||
// second editor. Without this limit everything will get
|
||||
// blocked indefinitely. How could this be fixed?
|
||||
for (int i = 0;
|
||||
i < 20 && PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE); i++) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
async_handle_events();
|
||||
});
|
||||
}
|
||||
|
||||
void GroupBridge::async_log_pipe_lines(
|
||||
boost::asio::posix::stream_descriptor& pipe,
|
||||
boost::asio::streambuf& buffer,
|
||||
std::string prefix) {
|
||||
boost::asio::async_read_until(
|
||||
pipe, buffer, '\n',
|
||||
[&, prefix](const boost::system::error_code& error, size_t) {
|
||||
// When we get an error code then that likely means that the pipe
|
||||
// has been clsoed and we have reached the end of the file
|
||||
if (error.failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
std::getline(std::istream(&buffer), line);
|
||||
logger.log(prefix + line);
|
||||
|
||||
async_log_pipe_lines(pipe, buffer, prefix);
|
||||
});
|
||||
}
|
||||
|
||||
boost::asio::local::stream_protocol::acceptor create_acceptor_if_inactive(
|
||||
boost::asio::io_context& io_context,
|
||||
boost::asio::local::stream_protocol::endpoint& endpoint) {
|
||||
// First try to listen on the endpoint normally
|
||||
try {
|
||||
return boost::asio::local::stream_protocol::acceptor(io_context,
|
||||
endpoint);
|
||||
} catch (const boost::system::system_error& error) {
|
||||
// If this failed, then either there is a stale socket file or another
|
||||
// process is already is already listening. In the last case we will
|
||||
// simply throw so the other process can handle the request.
|
||||
std::ifstream open_sockets("/proc/net/unix");
|
||||
std::string endpoint_path = endpoint.path();
|
||||
for (std::string line; std::getline(open_sockets, line);) {
|
||||
if (line.size() < endpoint_path.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string file = line.substr(line.size() - endpoint_path.size());
|
||||
if (file == endpoint_path) {
|
||||
// Another process is already listening, so we don't have to do
|
||||
// anything
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point we can remove the stale socket and start listening
|
||||
fs::remove(endpoint_path);
|
||||
return boost::asio::local::stream_protocol::acceptor(io_context,
|
||||
endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
std::string create_logger_prefix(const fs::path& socket_path) {
|
||||
// The group socket filename will be in the format
|
||||
// '/tmp/yabridge-group-<group_name>-<wine_prefix_id>-<architecture>.sock',
|
||||
// where Wine prefix ID is just Wine prefix ran through `std::hash` to
|
||||
// prevent collisions without needing complicated filenames. We want to
|
||||
// extract the group name.
|
||||
std::string socket_name =
|
||||
socket_path.filename().replace_extension().string();
|
||||
|
||||
std::smatch group_match;
|
||||
std::regex group_regexp("^yabridge-group-(.*)-[^-]+-[^-]+$",
|
||||
std::regex::ECMAScript);
|
||||
if (std::regex_match(socket_name, group_match, group_regexp)) {
|
||||
socket_name = group_match[1].str();
|
||||
|
||||
#ifdef __i386__
|
||||
// Mark 32-bit versions to avoid potential confusion caused by 32-bit
|
||||
// and regular 64-bit group processes with the same name running
|
||||
// alongside eachother
|
||||
socket_name += "-x32";
|
||||
#endif
|
||||
}
|
||||
|
||||
return "[" + socket_name + "] ";
|
||||
}
|
||||
|
||||
uint32_t WINAPI handle_plugin_dispatch_proxy(void* param) {
|
||||
// The Win32 API only allows you to pass a void pointer to threads, so we
|
||||
// need to use manual memory management.
|
||||
auto thread_params = static_cast<handle_plugin_dispatch_parameters*>(param);
|
||||
GroupBridge* instance = thread_params->first;
|
||||
GroupRequest parameters = thread_params->second;
|
||||
delete thread_params;
|
||||
|
||||
instance->handle_plugin_dispatch(parameters);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// 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 "../boost-fix.h"
|
||||
|
||||
#include <boost/asio/posix/stream_descriptor.hpp>
|
||||
#include <boost/asio/streambuf.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include "vst2.h"
|
||||
|
||||
/**
|
||||
* Encapsulate capturing the STDOUT or STDERR stream by opening a pipe and
|
||||
* reopening the passed file descriptor as one of the ends of the newly opened
|
||||
* pipe. This allows all output sent to be read from that pipe. This is needed
|
||||
* to capture all (debug) output from Wine and the hosted plugins so we can
|
||||
* prefix it with a timestamp and a group identifier and potentially write it to
|
||||
* a log file. Since the host application is run independently of the yabridge
|
||||
* instance that spawned it, this can't simply be done by the caller like we're
|
||||
* doing for Wine output in individually hosted plugins.
|
||||
*/
|
||||
class StdIoCapture {
|
||||
public:
|
||||
/**
|
||||
* Redirect all output sent to a file descriptor (e.g. `STDOUT_FILENO` or
|
||||
* `STDERR_FILENO`) to a pipe. `StdIoCapture::pipe` can be used to read from
|
||||
* this pipe.
|
||||
*
|
||||
* @param io_context The IO context to create the captured pipe stream on.
|
||||
* @param file_descriptor The file descriptor to remap.
|
||||
*/
|
||||
StdIoCapture(boost::asio::io_context& io_context, int file_descriptor);
|
||||
|
||||
StdIoCapture(const StdIoCapture&) = delete;
|
||||
StdIoCapture& operator=(const StdIoCapture&) = delete;
|
||||
|
||||
/**
|
||||
* On cleanup, close the outgoing file descriptor from the pipe and restore
|
||||
* the original file descriptor for the captured stream.
|
||||
*/
|
||||
~StdIoCapture();
|
||||
|
||||
/**
|
||||
* The pipe endpoint where all output from the original file descriptor gets
|
||||
* redirected to. This can be read from like any other `Boost.Asio` stream.
|
||||
*/
|
||||
boost::asio::posix::stream_descriptor pipe;
|
||||
|
||||
private:
|
||||
/**
|
||||
* The file descriptor of the stream we're capturing.
|
||||
*/
|
||||
const int target_fd;
|
||||
|
||||
/**
|
||||
* A copy of the original file descriptor. Will be used to undo
|
||||
* the capture when this object gets destroyed.
|
||||
*/
|
||||
const int original_fd_copy;
|
||||
|
||||
/**
|
||||
* The two file descriptors generated by the `pipe()` function call.
|
||||
* `pipe_fd[1]` is used to reopen/capture the passed file descriptor, and
|
||||
* `pipe_fd[0]` can be used to read the captured output from.
|
||||
*/
|
||||
int pipe_fd[2];
|
||||
};
|
||||
|
||||
/**
|
||||
* A 'plugin group' that listens on a _group socket_ for plugins to host in this
|
||||
* process. Once the plugin gets loaded into a new thread the actual bridging
|
||||
* process is identical to individually hosted plugins.
|
||||
*
|
||||
* An important detail worth mentioning here is that while this plugin group can
|
||||
* throw in the constructor when another process is already listening on the
|
||||
* socket, this should not be treated as an error. When using plugins groups,
|
||||
* yabridge will try to connect to the group socket on initialization and it
|
||||
* will launch a new group host process if it can't. If this is done for
|
||||
* multiple yabridge instances at the same time, then multiple group host
|
||||
* processes will be launched. Instead of using complicated inter-process
|
||||
* synchronization, we'll simply allow the processes to fail when another
|
||||
* process is already listening on the socket.
|
||||
*/
|
||||
class GroupBridge {
|
||||
public:
|
||||
/**
|
||||
* Create a plugin group by listening on the provided socket for incoming
|
||||
* plugin host requests.
|
||||
*
|
||||
* @param gruop_socket_path The path to the group socket endpoint. This path
|
||||
* should be in the form of
|
||||
* `/tmp/yabridge-group-<group_name>-<wine_prefix_id>-<architecture>.sock`
|
||||
* where `<wine_prefix_id>` is a numerical hash as explained in the
|
||||
* `create_logger_prefix()` function in `./group.cpp`.
|
||||
*
|
||||
* @throw boost::system::system_error If we can't listen on the socket.
|
||||
*
|
||||
* @note Creating an `GroupBridge` instance has the side effect that the
|
||||
* STDOUT and STDERR streams of the current process will be redirected to
|
||||
* a pipe so they can be properly written to a log file.
|
||||
*/
|
||||
GroupBridge(boost::filesystem::path group_socket_path);
|
||||
|
||||
~GroupBridge();
|
||||
|
||||
GroupBridge(const GroupBridge&) = delete;
|
||||
GroupBridge& operator=(const GroupBridge&) = delete;
|
||||
|
||||
/**
|
||||
* Run a plugin's dispatcher and message loop, processing all events on the
|
||||
* main IO context. The plugin will have already been created in
|
||||
* `accept_requests` since it has to be initiated inside of the IO context's
|
||||
* thread. Called by proxy using `handle_plugin_dispatch_proxy()` in
|
||||
* `./group.cpp` because the Win32 `CreateThread` API only allows passing a
|
||||
* single pointer to the function and does not allow lambdas.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* Listen for new requests to spawn plugins within this process and handle
|
||||
* them accordingly. Will terminate once all plugins have exited.
|
||||
*/
|
||||
void handle_incoming_connections();
|
||||
|
||||
/**
|
||||
* Returns true if the message loop should not be run at this time. This is
|
||||
* necessary because hosts will always call either `effEditOpen()` and then
|
||||
* `effEditGetRect()` or the other way around. If the message loop is
|
||||
* handled in between these two actions, then some plugins will either
|
||||
* freeze or sometimes outright crash. Because every plugin has to be run
|
||||
* from the same thread, this is a simple way to synchronize blocking the
|
||||
* mesage loop between the different plugin instances.
|
||||
*/
|
||||
bool should_skip_message_loop();
|
||||
|
||||
private:
|
||||
/**
|
||||
* Listen on the group socket for incoming requests to host a new plugin
|
||||
* within this group process. This will read a `GoupRequest` object
|
||||
* containing information about the plugin, reply with this process's PID so
|
||||
* the yabridge instance can tell if the plugin crashed during
|
||||
* initialization, and it will then try to initialize the plugin. After
|
||||
* intialization the plugin handling will be handed over to a new thread
|
||||
* running `handle_plugin_dispatch()`. Because of the way the Win32 API
|
||||
* works, all plugins have to be initialized from the same thread, and all
|
||||
* 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_multi()` function to run events within the same
|
||||
* `plugin_context`.
|
||||
*
|
||||
* @see handle_plugin_dispatch
|
||||
*/
|
||||
void accept_requests();
|
||||
|
||||
/**
|
||||
* Handle both Win32 messages and X11 events on a timer within the IO
|
||||
* context. This is a centralized replacement for the event handling in
|
||||
* `Vst2Bridge::handle_dispatch_single` for plugin groups.
|
||||
*/
|
||||
void async_handle_events();
|
||||
|
||||
/**
|
||||
* Continuously read from a pipe and write the output to the log file. Used
|
||||
* with the IO streams captured by `stdout_redirect` and `stderr_redirect`.
|
||||
*
|
||||
* TODO: Merge this with `PluginBridge::async_log_pipe_lines`
|
||||
*
|
||||
* @param pipe The pipe to read from.
|
||||
* @param buffer The stream buffer to write to.
|
||||
* @param prefix Text to prepend to the line before writing to the log.
|
||||
*/
|
||||
void async_log_pipe_lines(boost::asio::posix::stream_descriptor& pipe,
|
||||
boost::asio::streambuf& buffer,
|
||||
std::string prefix);
|
||||
|
||||
/**
|
||||
* The logging facility used for this group host process. Since we can't
|
||||
* identify which plugin is generating (debug) output, every line will only
|
||||
* be prefixed with the name of the group.
|
||||
*/
|
||||
Logger logger;
|
||||
|
||||
/**
|
||||
* The IO context that connections will be accepted on, and that any plugin
|
||||
* 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;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
boost::asio::io_context stdio_context;
|
||||
|
||||
boost::asio::streambuf stdout_buffer;
|
||||
boost::asio::streambuf stderr_buffer;
|
||||
/**
|
||||
* Contains a pipe used for capturing this process's STDOUT stream. Needed
|
||||
* to be able to process the output generated by Wine and plugins and to be
|
||||
* able write it write it to an external log file.
|
||||
*/
|
||||
StdIoCapture stdout_redirect;
|
||||
/**
|
||||
* Contains a pipe used for capturing this process's STDERR stream. Needed
|
||||
* to be able to process the output generated by Wine and plugins and to be
|
||||
* able write it write it to an external log file.
|
||||
*/
|
||||
StdIoCapture stderr_redirect;
|
||||
/**
|
||||
* A thread that runs the `stdio_context` loop.
|
||||
*/
|
||||
std::thread stdio_handler;
|
||||
|
||||
boost::asio::local::stream_protocol::endpoint group_socket_endpoint;
|
||||
/**
|
||||
* The UNIX domain socket acceptor that will be used to listen for incoming
|
||||
* connections to spawn new plugins within this process.
|
||||
*/
|
||||
boost::asio::local::stream_protocol::acceptor group_socket_acceptor;
|
||||
|
||||
/**
|
||||
* A map of threads that are currently hosting a plugin within this process
|
||||
* 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.
|
||||
*
|
||||
* TODO: Check again if we can just use std::thread here instead, that would
|
||||
* make everything much simpler. `std::thread` was a problem with
|
||||
* gdiplus in the past as Serum would randomly crash because calling
|
||||
* conventions were nto being respected.
|
||||
*/
|
||||
std::unordered_map<GroupRequest,
|
||||
std::pair<Win32Thread, std::unique_ptr<Vst2Bridge>>>
|
||||
active_plugins;
|
||||
/**
|
||||
* A mutex to prevent two threads from simultaneously accessing the plugins
|
||||
* map, and also to prevent `handle_plugin_dispatch()` from terminating the
|
||||
* process because it thinks there are no active plugins left just as a new
|
||||
* plugin is being spawned.
|
||||
*/
|
||||
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
|
||||
* plugin.
|
||||
*
|
||||
* @see handle_plugin_dispatch
|
||||
*/
|
||||
boost::asio::steady_timer shutdown_timer;
|
||||
};
|
||||
@@ -0,0 +1,559 @@
|
||||
// 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 "vst2.h"
|
||||
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
|
||||
#include "../../common/communication.h"
|
||||
#include "../../common/events.h"
|
||||
|
||||
/**
|
||||
* A function pointer to what should be the entry point of a VST plugin.
|
||||
*/
|
||||
using VstEntryPoint = AEffect*(VST_CALL_CONV*)(audioMasterCallback);
|
||||
|
||||
/**
|
||||
* This ugly global is needed so we can get the instance of a `Brdige` class
|
||||
* from an `AEffect` when it performs a host callback during its initialization.
|
||||
*/
|
||||
Vst2Bridge* current_bridge_instance = nullptr;
|
||||
/**
|
||||
* Needed for the rare event that two plugins are getting initialized at the
|
||||
* same time.
|
||||
*/
|
||||
std::mutex current_bridge_instance_mutex;
|
||||
|
||||
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
|
||||
* to avoid using globals since we need free function pointers to interface with
|
||||
* the VST C API.
|
||||
*/
|
||||
Vst2Bridge& get_bridge_instance(const AEffect* plugin) {
|
||||
// This is needed during the initialization of the plugin since we can only
|
||||
// add our own pointer after it's done initializing
|
||||
if (current_bridge_instance != nullptr) {
|
||||
return *current_bridge_instance;
|
||||
}
|
||||
|
||||
return *static_cast<Vst2Bridge*>(plugin->ptr1);
|
||||
}
|
||||
|
||||
Vst2Bridge::Vst2Bridge(std::string plugin_dll_path,
|
||||
std::string socket_endpoint_path)
|
||||
// See `plugin_handle`s docstring for information on why we're leaking
|
||||
// memory here
|
||||
// : plugin_handle(LoadLibrary(plugin_dll_path.c_str()), FreeLibrary),
|
||||
: plugin_handle(LoadLibrary(plugin_dll_path.c_str())),
|
||||
io_context(),
|
||||
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) {
|
||||
// Got to love these C APIs
|
||||
if (plugin_handle == nullptr) {
|
||||
throw std::runtime_error("Could not load the Windows .dll file at '" +
|
||||
plugin_dll_path + "'");
|
||||
}
|
||||
|
||||
// VST plugin entry point functions should be called `VSTPluginMain`, but
|
||||
// there are some older deprecated names that legacy plugins may still use
|
||||
VstEntryPoint vst_entry_point = nullptr;
|
||||
for (auto name : {"VSTPluginMain", "main_plugin", "main"}) {
|
||||
vst_entry_point = reinterpret_cast<VstEntryPoint>(
|
||||
reinterpret_cast<size_t>(GetProcAddress(plugin_handle, name)));
|
||||
|
||||
if (vst_entry_point != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (vst_entry_point == nullptr) {
|
||||
throw std::runtime_error(
|
||||
"Could not find a valid VST entry point for '" + plugin_dll_path +
|
||||
"'.");
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Initialize after communication has been set up
|
||||
// We'll try to do the same `get_bridge_isntance` trick as in
|
||||
// `plugin/plugin.cpp`, but since the plugin will probably call the host
|
||||
// callback while it's initializing we sadly have to use a global here.
|
||||
{
|
||||
std::lock_guard lock(current_bridge_instance_mutex);
|
||||
current_bridge_instance = this;
|
||||
plugin = vst_entry_point(host_callback_proxy);
|
||||
if (plugin == nullptr) {
|
||||
throw std::runtime_error("VST plugin at '" + plugin_dll_path +
|
||||
"' failed to initialize.");
|
||||
}
|
||||
|
||||
// We only needed this little hack during initialization
|
||||
current_bridge_instance = nullptr;
|
||||
plugin->ptr1 = this;
|
||||
}
|
||||
|
||||
// Send the plugin's information to the Linux VST plugin. This is done over
|
||||
// the `dispatch()` socket since this has to be done only once during
|
||||
// initialization. Any updates during runtime are handled using the
|
||||
// `audioMasterIOChanged` host callback.
|
||||
write_object(host_vst_dispatch, EventResult{0, *plugin, std::nullopt});
|
||||
|
||||
// This works functionally identically to the `handle_dispatch_single()`
|
||||
// function below, but this socket will only handle MIDI events. This is
|
||||
// needed because of Win32 API limitations.
|
||||
dispatch_midi_events_handler =
|
||||
Win32Thread(handle_dispatch_midi_events_proxy, this);
|
||||
|
||||
parameters_handler = Win32Thread(handle_parameters_proxy, this);
|
||||
|
||||
process_replacing_handler =
|
||||
Win32Thread(handle_process_replacing_proxy, this);
|
||||
}
|
||||
|
||||
bool Vst2Bridge::should_skip_message_loop() {
|
||||
return std::holds_alternative<EditorOpening>(editor);
|
||||
}
|
||||
|
||||
void Vst2Bridge::handle_dispatch_single() {
|
||||
using namespace std::placeholders;
|
||||
|
||||
// For our communication we use simple threads and blocking operations
|
||||
// instead of asynchronous IO since communication has to be handled in
|
||||
// lockstep anyway
|
||||
try {
|
||||
while (true) {
|
||||
receive_event(host_vst_dispatch, std::nullopt,
|
||||
passthrough_event(
|
||||
plugin, std::bind(&Vst2Bridge::dispatch_wrapper,
|
||||
this, _1, _2, _3, _4, _5, _6)));
|
||||
|
||||
handle_x11_events();
|
||||
handle_win32_events();
|
||||
}
|
||||
} catch (const boost::system::system_error&) {
|
||||
// The plugin has cut off communications, so we can shut down this host
|
||||
// application
|
||||
}
|
||||
}
|
||||
|
||||
void Vst2Bridge::handle_dispatch_multi(boost::asio::io_context& main_context) {
|
||||
// This works exactly the same as the function above, but execute the
|
||||
// actual event and run the message loop from the main thread that's
|
||||
// also instantiating these plugins. This is required for a few plugins
|
||||
// to run multiple instances in the same process
|
||||
try {
|
||||
while (true) {
|
||||
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 {
|
||||
std::promise<intptr_t> dispatch_result;
|
||||
boost::asio::dispatch(main_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
|
||||
}
|
||||
}
|
||||
|
||||
void Vst2Bridge::handle_dispatch_midi_events() {
|
||||
try {
|
||||
while (true) {
|
||||
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, nullptr,
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (const boost::system::system_error&) {
|
||||
// The plugin has cut off communications, so we can shut down this host
|
||||
// application
|
||||
}
|
||||
}
|
||||
|
||||
void Vst2Bridge::handle_parameters() {
|
||||
try {
|
||||
while (true) {
|
||||
// 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.has_value()) {
|
||||
// `setParameter`
|
||||
plugin->setParameter(plugin, request.index,
|
||||
request.value.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
|
||||
}
|
||||
}
|
||||
|
||||
void Vst2Bridge::handle_process_replacing() {
|
||||
std::vector<std::vector<float>> output_buffers(plugin->numOutputs);
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
auto request = read_object<AudioBuffers>(host_vst_process_replacing,
|
||||
process_buffer);
|
||||
|
||||
// The process functions expect a `float**` for their inputs and
|
||||
// their outputs
|
||||
std::vector<float*> inputs;
|
||||
for (auto& buffer : request.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
|
||||
std::vector<float*> outputs;
|
||||
output_buffers.resize(plugin->numOutputs);
|
||||
for (auto& buffer : output_buffers) {
|
||||
buffer.resize(request.sample_frames);
|
||||
outputs.push_back(buffer.data());
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// 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 != nullptr) {
|
||||
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) {
|
||||
std::fill(buffer.begin(), buffer.end(), 0.0);
|
||||
}
|
||||
|
||||
plugin->process(plugin, inputs.data(), outputs.data(),
|
||||
request.sample_frames);
|
||||
}
|
||||
|
||||
next_audio_buffer_midi_events.clear();
|
||||
}
|
||||
|
||||
AudioBuffers response{output_buffers, request.sample_frames};
|
||||
write_object(host_vst_process_replacing, response, process_buffer);
|
||||
}
|
||||
} catch (const boost::system::system_error&) {
|
||||
// The plugin has cut off communications, so we can shut down this host
|
||||
// application
|
||||
}
|
||||
}
|
||||
|
||||
intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin,
|
||||
int opcode,
|
||||
int index,
|
||||
intptr_t value,
|
||||
void* data,
|
||||
float option) {
|
||||
// We have to intercept GUI open calls since we can't use
|
||||
// the X11 window handle passed by the host
|
||||
switch (opcode) {
|
||||
case effEditGetRect: {
|
||||
// Some plugins will have a race condition if the message loops gets
|
||||
// handled between the call to `effEditGetRect()` and
|
||||
// `effEditOpen()`, although this behavior never appears on Windows
|
||||
// as hosts will always either call these functions in sequence or
|
||||
// in reverse. We need to temporarily stop handling messages when
|
||||
// this happens.
|
||||
if (!std::holds_alternative<Editor>(editor)) {
|
||||
editor = EditorOpening{};
|
||||
}
|
||||
|
||||
return plugin->dispatcher(plugin, opcode, index, value, data,
|
||||
option);
|
||||
} break;
|
||||
case effEditOpen: {
|
||||
// Create a Win32 window through Wine, embed it into the window
|
||||
// provided by the host, and let the plugin embed itself into
|
||||
// the Wine window
|
||||
const auto x11_handle = reinterpret_cast<size_t>(data);
|
||||
// Win32 window classes have to be unique for the whole application.
|
||||
// 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();
|
||||
Editor& editor_instance =
|
||||
editor.emplace<Editor>(window_class, plugin, x11_handle);
|
||||
|
||||
return plugin->dispatcher(plugin, opcode, index, value,
|
||||
editor_instance.win32_handle.get(),
|
||||
option);
|
||||
} break;
|
||||
case effEditClose: {
|
||||
const intptr_t return_value =
|
||||
plugin->dispatcher(plugin, opcode, index, value, data, option);
|
||||
|
||||
// Cleanup is handled through RAII
|
||||
editor = std::monostate();
|
||||
|
||||
return return_value;
|
||||
} break;
|
||||
default:
|
||||
return plugin->dispatcher(plugin, opcode, index, value, data,
|
||||
option);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Vst2Bridge::handle_win32_events() {
|
||||
std::visit(
|
||||
overload{[](Editor& editor) { editor.handle_win32_events(); },
|
||||
[](std::monostate&) {
|
||||
MSG msg;
|
||||
|
||||
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
},
|
||||
[](EditorOpening&) {
|
||||
// Don't handle any events in this
|
||||
// particular case as explained in
|
||||
// `Vst2Bridge::editor`
|
||||
}},
|
||||
editor);
|
||||
}
|
||||
|
||||
void Vst2Bridge::handle_x11_events() {
|
||||
std::visit(overload{[](Editor& editor) { editor.handle_x11_events(); },
|
||||
[](auto&) {}},
|
||||
editor);
|
||||
}
|
||||
|
||||
class HostCallbackDataConverter : DefaultDataConverter {
|
||||
public:
|
||||
HostCallbackDataConverter(AEffect* plugin,
|
||||
std::optional<VstTimeInfo>& time_info)
|
||||
: plugin(plugin), time_info(time_info) {}
|
||||
|
||||
EventPayload read(const int opcode,
|
||||
const int index,
|
||||
const intptr_t value,
|
||||
const void* data) {
|
||||
switch (opcode) {
|
||||
case audioMasterGetTime:
|
||||
return WantsVstTimeInfo{};
|
||||
break;
|
||||
case audioMasterIOChanged:
|
||||
// This is a helpful event that indicates that the VST
|
||||
// plugin's `AEffect` struct has changed. Writing these
|
||||
// results back is done inside of `passthrough_event`.
|
||||
return AEffect(*plugin);
|
||||
break;
|
||||
case audioMasterProcessEvents:
|
||||
return DynamicVstEvents(*static_cast<const VstEvents*>(data));
|
||||
break;
|
||||
// We detect whether an opcode should return a string by
|
||||
// checking whether there's a zeroed out buffer behind the void
|
||||
// pointer. This works for any host, but not all plugins zero
|
||||
// out their buffers.
|
||||
case audioMasterGetVendorString:
|
||||
case audioMasterGetProductString:
|
||||
return WantsString{};
|
||||
break;
|
||||
default:
|
||||
return DefaultDataConverter::read(opcode, index, value, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<EventPayload> read_value(const int opcode,
|
||||
const intptr_t value) {
|
||||
return DefaultDataConverter::read_value(opcode, value);
|
||||
}
|
||||
|
||||
void write(const int opcode, void* data, const EventResult& response) {
|
||||
switch (opcode) {
|
||||
case audioMasterGetTime:
|
||||
// Write the returned `VstTimeInfo` struct into a field and
|
||||
// make the function return a poitner to it in the function
|
||||
// below. Depending on whether the host supported the
|
||||
// requested time information this operations returns either
|
||||
// a null pointer or a pointer to a `VstTimeInfo` object.
|
||||
if (std::holds_alternative<std::nullptr_t>(response.payload)) {
|
||||
time_info = std::nullopt;
|
||||
} else {
|
||||
time_info = std::get<VstTimeInfo>(response.payload);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
DefaultDataConverter::write(opcode, data, response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
intptr_t return_value(const int opcode, const intptr_t original) {
|
||||
switch (opcode) {
|
||||
case audioMasterGetTime: {
|
||||
// Return a pointer to the `VstTimeInfo` object written in
|
||||
// the function above
|
||||
VstTimeInfo* time_info_pointer = nullptr;
|
||||
if (time_info.has_value()) {
|
||||
time_info_pointer = &time_info.value();
|
||||
}
|
||||
|
||||
return reinterpret_cast<intptr_t>(time_info_pointer);
|
||||
} break;
|
||||
default:
|
||||
return DefaultDataConverter::return_value(opcode, original);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void write_value(const int opcode,
|
||||
intptr_t value,
|
||||
const EventResult& response) {
|
||||
return DefaultDataConverter::write_value(opcode, value, response);
|
||||
}
|
||||
|
||||
private:
|
||||
AEffect* plugin;
|
||||
std::optional<VstTimeInfo>& time_info;
|
||||
};
|
||||
|
||||
intptr_t Vst2Bridge::host_callback(AEffect* effect,
|
||||
int opcode,
|
||||
int index,
|
||||
intptr_t value,
|
||||
void* data,
|
||||
float option) {
|
||||
HostCallbackDataConverter converter(effect, time_info);
|
||||
return send_event(vst_host_callback, host_callback_mutex, converter,
|
||||
std::nullopt, opcode, index, value, data, option);
|
||||
}
|
||||
|
||||
intptr_t VST_CALL_CONV host_callback_proxy(AEffect* effect,
|
||||
int opcode,
|
||||
int index,
|
||||
intptr_t value,
|
||||
void* data,
|
||||
float option) {
|
||||
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;
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "boost-fix.h"
|
||||
#include "../boost-fix.h"
|
||||
|
||||
#define NOMINMAX
|
||||
#define NOSERVICE
|
||||
@@ -29,23 +29,34 @@
|
||||
#include <boost/asio/local/stream_protocol.hpp>
|
||||
#include <mutex>
|
||||
|
||||
#include "../common/logging.h"
|
||||
#include "editor.h"
|
||||
#include "utils.h"
|
||||
#include "../../common/logging.h"
|
||||
#include "../editor.h"
|
||||
#include "../utils.h"
|
||||
|
||||
/**
|
||||
* A marker struct to indicate that the editor is about to be opened.
|
||||
*
|
||||
* @see WineBridge::editor
|
||||
* @see Vst2Bridge::editor
|
||||
*/
|
||||
struct EditorOpening {};
|
||||
|
||||
/**
|
||||
* This handles the communication between the Linux native VST plugin and the
|
||||
* Wine VST host. The functions below should be used as callback functions in an
|
||||
* `AEffect` object.
|
||||
* This hosts a Windows VST2 plugin, forwards messages sent by the Linux VST
|
||||
* 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. For individually hosted plugins this only means that
|
||||
* this class has to be initialized from the same thread as the one that calls
|
||||
* `handle_dispatch_single()`, and thus also runs the message loop. When using
|
||||
* plugin groups, however, all instantiation, editor event handling and
|
||||
* message loop pumping has to be done 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. When running multiple plugins
|
||||
* `handle_dispatch_multi()` should be used to make sure all plugins
|
||||
* handle their events on the same thread.
|
||||
*/
|
||||
class WineBridge {
|
||||
class Vst2Bridge {
|
||||
public:
|
||||
/**
|
||||
* Initializes the Windows VST plugin and set up communication with the
|
||||
@@ -56,27 +67,67 @@ class WineBridge {
|
||||
* @param socket_endpoint_path A (Unix style) path to the Unix socket
|
||||
* endpoint the native VST plugin created to communicate over.
|
||||
*
|
||||
* @note When using plugin groups and `handle_dispatch_multi()`, this
|
||||
* object has to be constructed from within the IO context.
|
||||
*
|
||||
* @throw std::runtime_error Thrown when the VST plugin could not be loaded,
|
||||
* or if communication could not be set up.
|
||||
*/
|
||||
WineBridge(std::string plugin_dll_path, std::string socket_endpoint_path);
|
||||
Vst2Bridge(std::string plugin_dll_path, std::string socket_endpoint_path);
|
||||
|
||||
/**
|
||||
* Returns true if the message loop should be skipped. This happens when the
|
||||
* editor is in the process of being opened. In VST hosts on Windows
|
||||
* `effEditOpen()` and `effEditGetRect()` will always be called in sequence,
|
||||
* but in our approach there will be an opportunity to handle events in
|
||||
* between these two calls. Most plugins will handle this just fine, but
|
||||
* some plugins end up blocking indefinitely while waiting for the other
|
||||
* function to be called, hence why this function is needed. For
|
||||
* individually hosted plugins this check is done implicitely in
|
||||
* `Vst2Bridge::handle_win32_events()`.
|
||||
*/
|
||||
bool should_skip_message_loop();
|
||||
|
||||
/**
|
||||
* Handle events on the main thread until the plugin quits. This can't be
|
||||
* done on another thread since some plugins (e.g. Melda) expect certain
|
||||
* (but for some reason not all) events to be passed from the same thread it
|
||||
* was initiated from. This is then also the same thread that should handle
|
||||
* Win32 GUI events.
|
||||
* events to be passed from the same thread it was initiated from. This is
|
||||
* then also the same thread that should handle Win32 GUI events.
|
||||
*/
|
||||
void handle_dispatch();
|
||||
void handle_dispatch_single();
|
||||
|
||||
// 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.
|
||||
[[noreturn]] void handle_dispatch_midi_events();
|
||||
[[noreturn]] void handle_parameters();
|
||||
[[noreturn]] void handle_process_replacing();
|
||||
/**
|
||||
* Handle events just like in the function above, but do the actual
|
||||
* execution on the IO context. As explained in this class' docstring, this
|
||||
* is needed because some plugins make the assumption that all of their
|
||||
* instances are handled from the same thread, and that the thread that the
|
||||
* first instance was initiated on will be kept alive until the VST host
|
||||
* terminates.
|
||||
*
|
||||
* @param main_context The main IO context that's handling the event
|
||||
* handling for all plugins.
|
||||
*
|
||||
* @note With this approach you'll have to make sure that the object was
|
||||
* instantiated from the same thread as the one that runs the IO context.
|
||||
* @note This appraoch does _not_ handle any events. This has to be done on
|
||||
* a timer within the IO context since otherwise things would become very
|
||||
* messy very quick.
|
||||
*/
|
||||
void handle_dispatch_multi(boost::asio::io_context& main_context);
|
||||
|
||||
/**
|
||||
* Handle X11 events for the editor window if it is open. This can be run
|
||||
* safely from any thread.
|
||||
*/
|
||||
void handle_x11_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
|
||||
@@ -105,11 +156,37 @@ class WineBridge {
|
||||
float option);
|
||||
|
||||
/**
|
||||
* The shared library handle of the VST plugin. I sadly could not get
|
||||
* Boost.DLL to work here, so we'll just load the VST plugisn by hand.
|
||||
* Run the message loop for this plugin and potentially also for other
|
||||
* plugins. This is only used in `handle_dispatch_single()`, as this will be
|
||||
* run on a timer when using plugin groups. The caller should first check
|
||||
* whether the event loop can be run through `should_skip_message_loop()`.
|
||||
*
|
||||
* Because of the way the Win32 API works we have to process events on the
|
||||
* same thread as the one the window was created on, and that thread is the
|
||||
* thread that's handling dispatcher calls. Some plugins will also rely on
|
||||
* the Win32 message loop to run tasks on a timer and to defer loading, so
|
||||
* we have to make sure to always run this loop. The only exception is a in
|
||||
* specific situation that can cause a race condition in some plugins
|
||||
* because of incorrect assumptions made by the plugin. See the dostring for
|
||||
* `Vst2Bridge::editor` for more information.
|
||||
*/
|
||||
std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&FreeLibrary)>
|
||||
plugin_handle;
|
||||
void handle_win32_events();
|
||||
|
||||
/**
|
||||
* The shared library handle of the VST plugin. I sadly could not get
|
||||
* Boost.DLL to work here, so we'll just load the VST plugins by hand.
|
||||
*
|
||||
* FIXME: I don't know why, but `FreeLibrary()` seems to corrupt memory.
|
||||
* This leads to a lot of weird behavior, such as plugins crashing as
|
||||
* soon as other plugins get loaded or calls to `LoadLibrary()`
|
||||
* returning null pointers while they would otherwise load fine
|
||||
* without the prior call to `FreeLibrary`. We are leaking memory
|
||||
* here until this is fixed, but it should not be a huge issue since
|
||||
* this leak only exists for plugin groups.
|
||||
*/
|
||||
// std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&FreeLibrary)>
|
||||
// plugin_handle;
|
||||
HMODULE plugin_handle;
|
||||
|
||||
/**
|
||||
* The loaded plugin's `AEffect` struct, obtained using the above library
|
||||
@@ -210,6 +287,8 @@ class WineBridge {
|
||||
* To work around this we'll use this third state to temporarily stop
|
||||
* processing Windows events in the one or two ticks between these two
|
||||
* events.
|
||||
*
|
||||
* @see should_postpone_message_loop
|
||||
*/
|
||||
std::variant<std::monostate, Editor, EditorOpening> editor;
|
||||
};
|
||||
+89
-61
@@ -87,19 +87,28 @@ Editor::Editor(const std::string& window_class_name,
|
||||
// The Win32 API will block the `DispatchMessage` call when opening e.g. a
|
||||
// dropdown, but it will still allow timers to be run so the GUI can still
|
||||
// update in the background. Because of this we send `effEditIdle` to the
|
||||
// plugin on a timer. The refresh rate is purposely fairly low since we
|
||||
// we'll also trigger this manually in `Editor::handle_events()` whenever
|
||||
// the plugin is not busy.
|
||||
// plugin on a timer. The refresh rate is purposely fairly low since the
|
||||
// host will call `effEditIdle()` explicitely when the plugin is not busy.
|
||||
// TODO: Add a `KillTimer()` now that we are hosting multiple plugins
|
||||
SetTimer(win32_handle.get(), idle_timer_id, 100, nullptr);
|
||||
|
||||
// We need to tell the Wine window it has been moved whenever the window
|
||||
// it's been embedded in gets moved around. In most cases this is
|
||||
// `parent_window`, but for instance REAPER reparents `parent_window` in
|
||||
// another window so we'll have to find the correct window first.
|
||||
// Because we're not using XEmbed Wine will interpret any local coordinates
|
||||
// as global coordinates. To work around this we'll tell the Wine window
|
||||
// it's located at its actual coordinates on screen rather than somewhere
|
||||
// within. For robustness's sake this should be done both when the actual
|
||||
// window the Wine window is embedded in (which may not be the parent
|
||||
// window) is moved or resized, and when the user moves his mouse over the
|
||||
// window. We also want to set keyboard focus when the user clicks on the
|
||||
// Windows since Bitwig 3.2 now explicitely requires this.
|
||||
const uint32_t topmost_event_mask = XCB_EVENT_MASK_STRUCTURE_NOTIFY;
|
||||
xcb_change_window_attributes(x11_connection.get(), topmost_window,
|
||||
XCB_CW_EVENT_MASK, &topmost_event_mask);
|
||||
xcb_flush(x11_connection.get());
|
||||
const uint32_t parent_event_mask =
|
||||
XCB_EVENT_MASK_FOCUS_CHANGE | XCB_EVENT_MASK_ENTER_WINDOW;
|
||||
xcb_change_window_attributes(x11_connection.get(), parent_window,
|
||||
XCB_CW_EVENT_MASK, &parent_event_mask);
|
||||
xcb_flush(x11_connection.get());
|
||||
|
||||
// Embed the Win32 window into the window provided by the host. Instead of
|
||||
// using the XEmbed protocol, we'll register a few events and manage the
|
||||
@@ -137,7 +146,7 @@ void Editor::send_idle_event() {
|
||||
plugin->dispatcher(plugin, effEditIdle, 0, 0, nullptr, 0);
|
||||
}
|
||||
|
||||
void Editor::handle_events() {
|
||||
void Editor::handle_win32_events() {
|
||||
MSG msg;
|
||||
|
||||
// The null value for the second argument is needed to handle interaction
|
||||
@@ -158,8 +167,9 @@ void Editor::handle_events() {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle X11 events
|
||||
void Editor::handle_x11_events() {
|
||||
// TODO: Initiating drag-and-drop in Serum _sometimes_ causes the GUI to
|
||||
// update while dragging while other times it does not. From all the
|
||||
// plugins I've tested this only happens in Serum though.
|
||||
@@ -167,66 +177,84 @@ void Editor::handle_events() {
|
||||
while ((generic_event = xcb_poll_for_event(x11_connection.get())) !=
|
||||
nullptr) {
|
||||
switch (generic_event->response_type & event_type_mask) {
|
||||
// We're listening for ConfigureNotify events on the topmost window
|
||||
// before the root window, i.e. the window that's actually going to
|
||||
// get dragged around the by the user. In most cases this is the
|
||||
// same as `parent_window`.
|
||||
case XCB_CONFIGURE_NOTIFY: {
|
||||
// We're purposely not using XEmbed. This has the consequence
|
||||
// that wine still thinks that any X and Y coordinates are
|
||||
// relative to the x11 window root instead of the parent window
|
||||
// provided by the DAW, causing all sorts of GUI interactions to
|
||||
// break. To alleviate this we'll just lie to Wine and tell it
|
||||
// that it's located at the parent window's location on the root
|
||||
// window. We also will keep the child window at its largest
|
||||
// possible size to allow for smooth resizing. This works
|
||||
// because the embedding hierarchy is DAW window -> Win32 window
|
||||
// (created in this class) -> VST plugin window created by the
|
||||
// plugin itself. In this case it doesn't matter that the Win32
|
||||
// window is larger than the part of the client area the plugin
|
||||
// draws to since any excess will be clipped off by the parent
|
||||
// window.
|
||||
const auto query_cookie =
|
||||
xcb_query_tree(x11_connection.get(), parent_window);
|
||||
xcb_window_t root = xcb_query_tree_reply(x11_connection.get(),
|
||||
query_cookie, nullptr)
|
||||
->root;
|
||||
// We're listening for `ConfigureNotify` events on the topmost
|
||||
// window before the root window, i.e. the window that's actually
|
||||
// going to get dragged around the by the user. In most cases this
|
||||
// is the same as `parent_window`. When either this window gets
|
||||
// moved, or when the user moves his mouse over our window, the
|
||||
// local coordinates should be updated. The additional `EnterWindow`
|
||||
// check is sometimes necessary for using multiple editor windows
|
||||
// within a single plugin group.
|
||||
case XCB_CONFIGURE_NOTIFY:
|
||||
case XCB_ENTER_NOTIFY:
|
||||
fix_local_coordinates();
|
||||
break;
|
||||
case XCB_FOCUS_IN:
|
||||
fix_local_coordinates();
|
||||
|
||||
// We can't directly use the `event.x` and `event.y` coordinates
|
||||
// because the parent window may also be embedded inside another
|
||||
// window.
|
||||
const auto translate_cookie = xcb_translate_coordinates(
|
||||
x11_connection.get(), parent_window, root, 0, 0);
|
||||
const xcb_translate_coordinates_reply_t*
|
||||
translated_coordiantes = xcb_translate_coordinates_reply(
|
||||
x11_connection.get(), translate_cookie, nullptr);
|
||||
|
||||
xcb_configure_notify_event_t translated_event{};
|
||||
translated_event.response_type = XCB_CONFIGURE_NOTIFY;
|
||||
translated_event.event = child_window;
|
||||
translated_event.window = child_window;
|
||||
// This should be set to the same sizes the window was created
|
||||
// on. Since we're not using `SetWindowPos` to resize the
|
||||
// Window, Wine can get a bit confused when we suddenly report a
|
||||
// different client area size. Without this certain plugins
|
||||
// (such as those by Valhalla DSP) would break.
|
||||
translated_event.width = client_area.width;
|
||||
translated_event.height = client_area.height;
|
||||
translated_event.x = translated_coordiantes->dst_x;
|
||||
translated_event.y = translated_coordiantes->dst_y;
|
||||
|
||||
xcb_send_event(x11_connection.get(), false, child_window,
|
||||
XCB_EVENT_MASK_STRUCTURE_NOTIFY |
|
||||
XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
|
||||
reinterpret_cast<char*>(&translated_event));
|
||||
// Explicitely request input focus when the user clicks on the
|
||||
// window. This is needed for Bitwig Studio 3.2, as the parent
|
||||
// window now captures all keyboard events and forwards them to
|
||||
// the main Bitwig Studio window instead of allowing the child
|
||||
// window to handle those events.
|
||||
xcb_set_input_focus(x11_connection.get(),
|
||||
XCB_INPUT_FOCUS_PARENT, child_window,
|
||||
XCB_CURRENT_TIME);
|
||||
xcb_flush(x11_connection.get());
|
||||
} break;
|
||||
break;
|
||||
}
|
||||
|
||||
free(generic_event);
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::fix_local_coordinates() {
|
||||
// We're purposely not using XEmbed. This has the consequence that wine
|
||||
// still thinks that any X and Y coordinates are relative to the x11 window
|
||||
// root instead of the parent window provided by the DAW, causing all sorts
|
||||
// of GUI interactions to break. To alleviate this we'll just lie to Wine
|
||||
// and tell it that it's located at the parent window's location on the root
|
||||
// window. We also will keep the child window at its largest possible size
|
||||
// to allow for smooth resizing. This works because the embedding hierarchy
|
||||
// is DAW window -> Win32 window (created in this class) -> VST plugin
|
||||
// window created by the plugin itself. In this case it doesn't matter that
|
||||
// the Win32 window is larger than the part of the client area the plugin
|
||||
// draws to since any excess will be clipped off by the parent window.
|
||||
const auto query_cookie =
|
||||
xcb_query_tree(x11_connection.get(), parent_window);
|
||||
xcb_window_t root =
|
||||
xcb_query_tree_reply(x11_connection.get(), query_cookie, nullptr)->root;
|
||||
|
||||
// We can't directly use the `event.x` and `event.y` coordinates because the
|
||||
// parent window may also be embedded inside another window.
|
||||
// TODO: This seems to get clamped at (0, 0), causing large windows dragged
|
||||
// off screen on the top or the left borders to act up
|
||||
const auto translate_cookie = xcb_translate_coordinates(
|
||||
x11_connection.get(), parent_window, root, 0, 0);
|
||||
const xcb_translate_coordinates_reply_t* translated_coordiantes =
|
||||
xcb_translate_coordinates_reply(x11_connection.get(), translate_cookie,
|
||||
nullptr);
|
||||
|
||||
xcb_configure_notify_event_t translated_event{};
|
||||
translated_event.response_type = XCB_CONFIGURE_NOTIFY;
|
||||
translated_event.event = child_window;
|
||||
translated_event.window = child_window;
|
||||
// This should be set to the same sizes the window was created on. Since
|
||||
// we're not using `SetWindowPos` to resize the Window, Wine can get a bit
|
||||
// confused when we suddenly report a different client area size. Without
|
||||
// this certain plugins (such as those by Valhalla DSP) would break.
|
||||
translated_event.width = client_area.width;
|
||||
translated_event.height = client_area.height;
|
||||
translated_event.x = translated_coordiantes->dst_x;
|
||||
translated_event.y = translated_coordiantes->dst_y;
|
||||
|
||||
xcb_send_event(
|
||||
x11_connection.get(), false, child_window,
|
||||
XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
|
||||
reinterpret_cast<char*>(&translated_event));
|
||||
xcb_flush(x11_connection.get());
|
||||
}
|
||||
|
||||
LRESULT CALLBACK window_proc(HWND handle,
|
||||
UINT message,
|
||||
WPARAM wParam,
|
||||
|
||||
+16
-4
@@ -101,13 +101,25 @@ class Editor {
|
||||
void send_idle_event();
|
||||
|
||||
/**
|
||||
* Pump messages from the editor GUI's event loop until all events are
|
||||
* process. Must be run from the same thread the GUI was created in because
|
||||
* of Win32 limitations.
|
||||
* Pump messages from the editor loop loop until all events are process.
|
||||
* Must be run from the same thread the GUI was created in because of Win32
|
||||
* limitations.
|
||||
*/
|
||||
void handle_events();
|
||||
void handle_win32_events();
|
||||
|
||||
/**
|
||||
* Handle X11 events sent to the window our editor is embedded in.
|
||||
*/
|
||||
void handle_x11_events();
|
||||
|
||||
private:
|
||||
/**
|
||||
* Lie to the Wine window about its coordinates on the screen for
|
||||
* reparenting without using XEmbed. See the comment at the top of the
|
||||
* implementation on why this is needed.
|
||||
*/
|
||||
void fix_local_coordinates();
|
||||
|
||||
/**
|
||||
* A pointer to the currently active window. Will be a null pointer if no
|
||||
* window is active.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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 "boost-fix.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
// Generated inside of build directory
|
||||
#include <src/common/config/config.h>
|
||||
#include <src/common/config/version.h>
|
||||
|
||||
#include "bridges/group.h"
|
||||
#include "bridges/vst2.h"
|
||||
|
||||
/**
|
||||
* This works very similar to the host application defined in
|
||||
* `individual-host.cpp`, but instead of just loading a single plugin this will
|
||||
* act as a daemon that can host multiple 'grouped' plugins. This works by
|
||||
* allowing the `libyabridge.so` instance to connect this this process over a
|
||||
* socket to ask this process to host a VST `.dll` file using a provided socket.
|
||||
* After that initialization step both the regular individual plugin host and
|
||||
* this group plugin host will function identically on both the plugin and the
|
||||
* Wine VST host side.
|
||||
*
|
||||
* The explicit calling convention is needed to work around a bug introduced in
|
||||
* Wine 5.7: https://bugs.winehq.org/show_bug.cgi?id=49138
|
||||
*/
|
||||
int __cdecl main(int argc, char* argv[]) {
|
||||
// Instead of directly hosting a plugin, this process will receive a UNIX
|
||||
// domain socket endpoint path that it should listen on to allow yabridge
|
||||
// instances to spawn plugins in this process.
|
||||
if (argc < 2) {
|
||||
std::cerr << "Usage: "
|
||||
#ifdef __i386__
|
||||
<< yabridge_group_host_name_32bit
|
||||
#else
|
||||
<< yabridge_group_host_name
|
||||
#endif
|
||||
<< " <unix_domain_socket>" << std::endl;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string group_socket_endpoint_path(argv[1]);
|
||||
|
||||
std::cerr << "Initializing yabridge group host version "
|
||||
<< yabridge_git_version
|
||||
#ifdef __i386__
|
||||
<< " (32-bit compatibility mode)"
|
||||
#endif
|
||||
<< std::endl;
|
||||
|
||||
try {
|
||||
GroupBridge bridge(group_socket_endpoint_path);
|
||||
|
||||
// Blocks the main thread until all plugins have exited
|
||||
bridge.handle_incoming_connections();
|
||||
} catch (const boost::system::system_error& error) {
|
||||
// If another process is already listening on the socket, we'll just
|
||||
// print a message and exit quietly. This could happen if the host
|
||||
// starts multiple yabridge instances that all use the same plugin group
|
||||
// at the same time.
|
||||
// TODO: Check if this is printed on the right stream
|
||||
std::cerr << "Another process is already listening on this group's "
|
||||
"socket, connecting to the existing process:"
|
||||
<< std::endl;
|
||||
std::cerr << error.what() << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,16 @@
|
||||
#include <src/common/config/config.h>
|
||||
#include <src/common/config/version.h>
|
||||
|
||||
#include "wine-bridge.h"
|
||||
#include "bridges/vst2.h"
|
||||
|
||||
// This explicit calling convention is needed to work around a bug introduced in
|
||||
// Wine 5.7
|
||||
// https://bugs.winehq.org/show_bug.cgi?id=49138
|
||||
/**
|
||||
* 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
|
||||
* this over the socket.
|
||||
*
|
||||
* The explicit calling convention is needed to work around a bug introduced in
|
||||
* Wine 5.7: https://bugs.winehq.org/show_bug.cgi?id=49138
|
||||
*/
|
||||
int __cdecl main(int argc, char* argv[]) {
|
||||
// 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
|
||||
@@ -32,9 +37,9 @@ int __cdecl main(int argc, char* argv[]) {
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: "
|
||||
#ifdef __i386__
|
||||
<< yabridge_wine_host_name_32bit
|
||||
<< yabridge_individual_host_name_32bit
|
||||
#else
|
||||
<< yabridge_wine_host_name
|
||||
<< yabridge_individual_host_name
|
||||
#endif
|
||||
<< " <vst_plugin_dll> <unix_domain_socket>" << std::endl;
|
||||
|
||||
@@ -49,13 +54,14 @@ int __cdecl main(int argc, char* argv[]) {
|
||||
<< " (32-bit compatibility mode)"
|
||||
#endif
|
||||
<< std::endl;
|
||||
|
||||
try {
|
||||
WineBridge bridge(plugin_dll_path, socket_endpoint_path);
|
||||
Vst2Bridge bridge(plugin_dll_path, socket_endpoint_path);
|
||||
std::cerr << "Finished initializing '" << plugin_dll_path << "'"
|
||||
<< std::endl;
|
||||
|
||||
// Blocks the main thread until the plugin shuts down
|
||||
bridge.handle_dispatch();
|
||||
bridge.handle_dispatch_single();
|
||||
} catch (const std::runtime_error& error) {
|
||||
std::cerr << "Error while initializing Wine VST host:" << std::endl;
|
||||
std::cerr << error.what() << std::endl;
|
||||
@@ -1,486 +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/>.
|
||||
|
||||
#include "wine-bridge.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "../common/communication.h"
|
||||
#include "../common/events.h"
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
/**
|
||||
* A function pointer to what should be the entry point of a VST plugin.
|
||||
*/
|
||||
using VstEntryPoint = AEffect*(VST_CALL_CONV*)(audioMasterCallback);
|
||||
|
||||
/**
|
||||
* This ugly global is needed so we can get the instance of a `Brdige` class
|
||||
* from an `AEffect` when it performs a host callback during its initialization.
|
||||
*/
|
||||
WineBridge* current_bridge_instance = nullptr;
|
||||
|
||||
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 WineBridge instance stored in one of the two pointers reserved
|
||||
* for the host of the hosted VST plugin. This is sadly needed as a workaround
|
||||
* to avoid using globals since we need free function pointers to interface with
|
||||
* the VST C API.
|
||||
*/
|
||||
WineBridge& get_bridge_instance(const AEffect* plugin) {
|
||||
// This is needed during the initialization of the plugin since we can only
|
||||
// add our own pointer after it's done initializing
|
||||
if (current_bridge_instance != nullptr) {
|
||||
// This should only be used during initialization
|
||||
assert(plugin == nullptr || plugin->ptr1 == nullptr);
|
||||
return *current_bridge_instance;
|
||||
}
|
||||
|
||||
return *static_cast<WineBridge*>(plugin->ptr1);
|
||||
}
|
||||
|
||||
WineBridge::WineBridge(std::string plugin_dll_path,
|
||||
std::string socket_endpoint_path)
|
||||
: plugin_handle(LoadLibrary(plugin_dll_path.c_str()), FreeLibrary),
|
||||
io_context(),
|
||||
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) {
|
||||
// Got to love these C APIs
|
||||
if (plugin_handle == nullptr) {
|
||||
throw std::runtime_error("Could not load the Windows .dll file at '" +
|
||||
plugin_dll_path + "'");
|
||||
}
|
||||
|
||||
// VST plugin entry point functions should be called `VSTPluginMain`, but
|
||||
// there are some older deprecated names that legacy plugins may still use
|
||||
VstEntryPoint vst_entry_point = nullptr;
|
||||
for (auto name : {"VSTPluginMain", "main_plugin", "main"}) {
|
||||
vst_entry_point =
|
||||
reinterpret_cast<VstEntryPoint>(reinterpret_cast<size_t>(
|
||||
GetProcAddress(plugin_handle.get(), name)));
|
||||
|
||||
if (vst_entry_point != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (vst_entry_point == nullptr) {
|
||||
throw std::runtime_error(
|
||||
"Could not find a valid VST entry point for '" + plugin_dll_path +
|
||||
"'.");
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Initialize after communication has been set up
|
||||
// We'll try to do the same `get_bridge_isntance` trick as in
|
||||
// `plugin/plugin.cpp`, but since the plugin will probably call the host
|
||||
// callback while it's initializing we sadly have to use a global here.
|
||||
current_bridge_instance = this;
|
||||
plugin = vst_entry_point(host_callback_proxy);
|
||||
if (plugin == nullptr) {
|
||||
throw std::runtime_error("VST plugin at '" + plugin_dll_path +
|
||||
"' failed to initialize.");
|
||||
}
|
||||
|
||||
// We only needed this little hack during initialization
|
||||
current_bridge_instance = nullptr;
|
||||
plugin->ptr1 = this;
|
||||
|
||||
// Send the plugin's information to the Linux VST plugin. This is done over
|
||||
// the `dispatch()` socket since this has to be done only once during
|
||||
// initialization. Any updates during runtime are handled using the
|
||||
// `audioMasterIOChanged` host callback.
|
||||
write_object(host_vst_dispatch, EventResult{0, *plugin, std::nullopt});
|
||||
|
||||
// This works functionally identically to the `handle_dispatch()` function
|
||||
// below, but this socket will only handle MIDI events. This is needed
|
||||
// because of Win32 API limitations.
|
||||
dispatch_midi_events_handler =
|
||||
Win32Thread(handle_dispatch_midi_events_proxy, this);
|
||||
|
||||
parameters_handler = Win32Thread(handle_parameters_proxy, this);
|
||||
|
||||
process_replacing_handler =
|
||||
Win32Thread(handle_process_replacing_proxy, this);
|
||||
}
|
||||
|
||||
void WineBridge::handle_dispatch() {
|
||||
using namespace std::placeholders;
|
||||
|
||||
// For our communication we use simple threads and blocking operations
|
||||
// instead of asynchronous IO since communication has to be handled in
|
||||
// lockstep anyway
|
||||
try {
|
||||
while (true) {
|
||||
receive_event(host_vst_dispatch, std::nullopt,
|
||||
passthrough_event(
|
||||
plugin, std::bind(&WineBridge::dispatch_wrapper,
|
||||
this, _1, _2, _3, _4, _5, _6)));
|
||||
|
||||
// Because of the way the Win32 API works we have to process events
|
||||
// on the same thread as the one the window was created on, and that
|
||||
// thread is the thread that's handling dispatcher calls. Some
|
||||
// plugins will also rely on the Win32 message loop to run tasks on
|
||||
// a timer and to defer loading, so we have to make sure to always
|
||||
// run this loop. The only exception is a in specific situation that
|
||||
// can cause a race condition in some plugins because of incorrect
|
||||
// assumptions made by the plugin. See the dostring for
|
||||
// `WineBridge::editor` for more information.
|
||||
std::visit(overload{[](Editor& editor) { editor.handle_events(); },
|
||||
[](std::monostate&) {
|
||||
MSG msg;
|
||||
|
||||
while (PeekMessage(&msg, nullptr, 0, 0,
|
||||
PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
},
|
||||
[](EditorOpening&) {
|
||||
// Don't handle any events in this
|
||||
// particular case as explained in
|
||||
// `WineBridge::editor`
|
||||
}},
|
||||
editor);
|
||||
}
|
||||
} catch (const boost::system::system_error&) {
|
||||
// The plugin has cut off communications, so we can shut down this host
|
||||
// application
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] void WineBridge::handle_dispatch_midi_events() {
|
||||
while (true) {
|
||||
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, nullptr, 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(&WineBridge::dispatch_wrapper, this,
|
||||
_1, _2, _3, _4, _5, _6))(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] void WineBridge::handle_parameters() {
|
||||
while (true) {
|
||||
// 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.has_value()) {
|
||||
// `setParameter`
|
||||
plugin->setParameter(plugin, request.index, request.value.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] void WineBridge::handle_process_replacing() {
|
||||
std::vector<std::vector<float>> output_buffers(plugin->numOutputs);
|
||||
|
||||
while (true) {
|
||||
auto request = read_object<AudioBuffers>(host_vst_process_replacing,
|
||||
process_buffer);
|
||||
|
||||
// The process functions expect a `float**` for their inputs and
|
||||
// their outputs
|
||||
std::vector<float*> inputs;
|
||||
for (auto& buffer : request.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
|
||||
std::vector<float*> outputs;
|
||||
output_buffers.resize(plugin->numOutputs);
|
||||
for (auto& buffer : output_buffers) {
|
||||
buffer.resize(request.sample_frames);
|
||||
outputs.push_back(buffer.data());
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// 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 != nullptr) {
|
||||
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) {
|
||||
std::fill(buffer.begin(), buffer.end(), 0.0);
|
||||
}
|
||||
|
||||
plugin->process(plugin, inputs.data(), outputs.data(),
|
||||
request.sample_frames);
|
||||
}
|
||||
|
||||
next_audio_buffer_midi_events.clear();
|
||||
}
|
||||
|
||||
AudioBuffers response{output_buffers, request.sample_frames};
|
||||
write_object(host_vst_process_replacing, response, process_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
intptr_t WineBridge::dispatch_wrapper(AEffect* plugin,
|
||||
int opcode,
|
||||
int index,
|
||||
intptr_t value,
|
||||
void* data,
|
||||
float option) {
|
||||
// We have to intercept GUI open calls since we can't use
|
||||
// the X11 window handle passed by the host
|
||||
switch (opcode) {
|
||||
case effEditGetRect: {
|
||||
// Some plugins will have a race condition if the message loops gets
|
||||
// handled between the call to `effEditGetRect()` and
|
||||
// `effEditOpen()`, although this behavior never appears on Windows
|
||||
// as hosts will always either call these functions in sequence or
|
||||
// in reverse. We need to temporarily stop handling messages when
|
||||
// this happens.
|
||||
if (!std::holds_alternative<Editor>(editor)) {
|
||||
editor = EditorOpening{};
|
||||
}
|
||||
|
||||
return plugin->dispatcher(plugin, opcode, index, value, data,
|
||||
option);
|
||||
} break;
|
||||
case effEditOpen: {
|
||||
// Create a Win32 window through Wine, embed it into the window
|
||||
// provided by the host, and let the plugin embed itself into
|
||||
// the Wine window
|
||||
const auto x11_handle = reinterpret_cast<size_t>(data);
|
||||
Editor& editor_instance =
|
||||
editor.emplace<Editor>("yabridge plugin", plugin, x11_handle);
|
||||
|
||||
return plugin->dispatcher(plugin, opcode, index, value,
|
||||
editor_instance.win32_handle.get(),
|
||||
option);
|
||||
} break;
|
||||
case effEditClose: {
|
||||
const intptr_t return_value =
|
||||
plugin->dispatcher(plugin, opcode, index, value, data, option);
|
||||
|
||||
// Cleanup is handled through RAII
|
||||
editor = std::monostate();
|
||||
|
||||
return return_value;
|
||||
} break;
|
||||
default:
|
||||
return plugin->dispatcher(plugin, opcode, index, value, data,
|
||||
option);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
class HostCallbackDataConverter : DefaultDataConverter {
|
||||
public:
|
||||
HostCallbackDataConverter(AEffect* plugin,
|
||||
std::optional<VstTimeInfo>& time_info)
|
||||
: plugin(plugin), time_info(time_info) {}
|
||||
|
||||
EventPayload read(const int opcode,
|
||||
const int index,
|
||||
const intptr_t value,
|
||||
const void* data) {
|
||||
switch (opcode) {
|
||||
case audioMasterGetTime:
|
||||
return WantsVstTimeInfo{};
|
||||
break;
|
||||
case audioMasterIOChanged:
|
||||
// This is a helpful event that indicates that the VST plugin's
|
||||
// `AEffect` struct has changed. Writing these results back is
|
||||
// done inside of `passthrough_event`.
|
||||
return AEffect(*plugin);
|
||||
break;
|
||||
case audioMasterProcessEvents:
|
||||
return DynamicVstEvents(*static_cast<const VstEvents*>(data));
|
||||
break;
|
||||
// We detect whether an opcode should return a string by checking
|
||||
// whether there's a zeroed out buffer behind the void pointer. This
|
||||
// works for any host, but not all plugins zero out their buffers.
|
||||
case audioMasterGetVendorString:
|
||||
case audioMasterGetProductString:
|
||||
return WantsString{};
|
||||
break;
|
||||
default:
|
||||
return DefaultDataConverter::read(opcode, index, value, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<EventPayload> read_value(const int opcode,
|
||||
const intptr_t value) {
|
||||
return DefaultDataConverter::read_value(opcode, value);
|
||||
}
|
||||
|
||||
void write(const int opcode, void* data, const EventResult& response) {
|
||||
switch (opcode) {
|
||||
case audioMasterGetTime:
|
||||
// Write the returned `VstTimeInfo` struct into a field and make
|
||||
// the function return a poitner to it in the function below.
|
||||
// Depending on whether the host supported the requested time
|
||||
// information this operations returns either a null pointer or
|
||||
// a pointer to a `VstTimeInfo` object.
|
||||
if (std::holds_alternative<std::nullptr_t>(response.payload)) {
|
||||
time_info = std::nullopt;
|
||||
} else {
|
||||
time_info = std::get<VstTimeInfo>(response.payload);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
DefaultDataConverter::write(opcode, data, response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
intptr_t return_value(const int opcode, const intptr_t original) {
|
||||
switch (opcode) {
|
||||
case audioMasterGetTime: {
|
||||
// Return a pointer to the `VstTimeInfo` object written in the
|
||||
// function above
|
||||
VstTimeInfo* time_info_pointer = nullptr;
|
||||
if (time_info.has_value()) {
|
||||
time_info_pointer = &time_info.value();
|
||||
}
|
||||
|
||||
return reinterpret_cast<intptr_t>(time_info_pointer);
|
||||
} break;
|
||||
default:
|
||||
return DefaultDataConverter::return_value(opcode, original);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void write_value(const int opcode,
|
||||
intptr_t value,
|
||||
const EventResult& response) {
|
||||
return DefaultDataConverter::write_value(opcode, value, response);
|
||||
}
|
||||
|
||||
private:
|
||||
AEffect* plugin;
|
||||
std::optional<VstTimeInfo>& time_info;
|
||||
};
|
||||
|
||||
intptr_t WineBridge::host_callback(AEffect* effect,
|
||||
int opcode,
|
||||
int index,
|
||||
intptr_t value,
|
||||
void* data,
|
||||
float option) {
|
||||
HostCallbackDataConverter converter(effect, time_info);
|
||||
return send_event(vst_host_callback, host_callback_mutex, converter,
|
||||
std::nullopt, opcode, index, value, data, option);
|
||||
}
|
||||
|
||||
intptr_t VST_CALL_CONV host_callback_proxy(AEffect* effect,
|
||||
int opcode,
|
||||
int index,
|
||||
intptr_t value,
|
||||
void* data,
|
||||
float option) {
|
||||
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<WineBridge*>(instance)->handle_dispatch_midi_events();
|
||||
}
|
||||
|
||||
uint32_t WINAPI handle_parameters_proxy(void* instance) {
|
||||
static_cast<WineBridge*>(instance)->handle_parameters();
|
||||
}
|
||||
|
||||
uint32_t WINAPI handle_process_replacing_proxy(void* instance) {
|
||||
static_cast<WineBridge*>(instance)->handle_process_replacing();
|
||||
}
|
||||
@@ -1 +1,4 @@
|
||||
/*/*
|
||||
|
||||
# The above pattern doesn't match submodules
|
||||
/tomlplusplus
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/marzer/tomlplusplus.git
|
||||
revision = v1.2.5
|
||||
depth = 1
|
||||
Reference in New Issue
Block a user