diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad7cf00d..0e796134 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: - name: Create an archive for the binaries run: | mkdir yabridge - cp build/libyabridge.so build/yabridge-{host,group}{,-32}.exe{,.so} yabridge + cp build/libyabridge-vst{2,3}.so build/yabridge-{host,group}{,-32}.exe{,.so} yabridge cp CHANGELOG.md README.md yabridge tar -caf "$ARCHIVE_NAME" yabridge @@ -92,7 +92,7 @@ jobs: - name: Create an archive for the binaries run: | mkdir yabridge - cp build/libyabridge.so build/yabridge-{host,group}{,-32}.exe{,.so} yabridge + cp build/libyabridge-vst{2,3}.so build/yabridge-{host,group}{,-32}.exe{,.so} yabridge cp CHANGELOG.md README.md yabridge tar -caf "$ARCHIVE_NAME" yabridge diff --git a/CHANGELOG.md b/CHANGELOG.md index f6c51f45..66d4564f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,60 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +TODO: Remove the mentions of VST3 support not yet being part of the released version from the readme +TODO: Add an updates screenshot with some fancy VST3-only plugins to the readme + +### Added + +- Yabridge 3.0 introduces the first ever true Wine VST3 bridge, allowing you to + use Windows VST3 plugins in Linux VST3 hosts with full VST 3.7.1 + compatibility. Simply tell yabridgectl to look for plugins in + `$HOME/.wine/drive_c/Program Files/Common Files/VST3`, run `yabridgectl sync`, + and your VST3 compatible DAW will pick up the new plugins in + `~/.vst3/yabridge` automatically. Even though this feature has been tested + extensively with a variety of VST3 plugins and hosts, there's still a large + part of the VST 3.7.1 specification that none of the hosts or plugins we came + across actually used, so please let me know if you run into any weird + behaviour! +- Added the `with-vst3` compile time option to control whether yabridge should + be built with VST3 support. This is enabled by default. +- Added an + [option](https://github.com/robbert-vdh/yabridge#compatibility-options) to use + Wine's XEmbed implementation instead of yabridge's normal window embedding + method. Some plugins have will have redrawing issues when using XEmbed or the + editor might not show up at all, so your mileage may very much vary. + ### Changed +- `libyabridge.so` is now called `libyabridge-vst2.so`. If you're using + yabridgectl then nothing changes here. **To avoid any confusion in the future, + please remove the old `libyabridge.so` file before upgrading.** +- Slightly increased resposniveness when resizing plugin GUIs by preventing + unnecessary blitting. This also reduces flickering with plugins that don't do + double buffering. +- VST2 editor idle events are now handled slightly differently. This should + result in even more responsive GUIs and I have not come across any plugins + where this change introduced issues, but please let me know if it does break + anything for you. - Changed part of the build process considering [this Wine bug](https://bugs.winehq.org/show_bug.cgi?id=49138). Building with Wine 5.7 and 5.8 required a change, but that change now breaks builds using Wine 6.0 - and up. We now detect which version of Wine is used to build with, and we then - apply the change conditionally to be able to support building with both older - and newer versions of Wine. + and up. The build process now detect which version of Wine is used to build + with, and it then applies the change conditionally to be able to support + building with both older and newer versions of Wine. + +### Fixed + +- Added a background to the editor window to get rid of artifacts that would + occur if the plugin or the host didn't resize the window correctly. + +### yabridgectl + +- Updated for the changes in yabridge 3.0. Yabridgectl now allows you to set up + yabridge for VST3 plugins. Since `libyabridge.so` got renamed to + `libyabridge-vst2.so` in this version, it's advised to carefully remove the + old `libyabridge.so` and `yabridgectl` files before upgrading to avoid + confusing situations. ## [2.2.1] - 2020-12-12 diff --git a/README.md b/README.md index c84511b7..e13c339c 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,15 @@ [![Discord](https://img.shields.io/discord/786993304197267527.svg?label=Discord&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/pyNeweqadf) Yet Another way to use Windows VST plugins on Linux. Yabridge seamlessly -supports running both 64-bit Windows VST2 plugins as well as 32-bit Windows VST2 -plugins in a 64-bit Linux VST host, with optional support for inter-plugin -communication through [plugin groups](#plugin-groups). Its modern concurrent -architecture and focus on transparency allows yabridge to be both fast and -highly compatible, while also staying easy to debug and maintain. +supports using both 32-bit and 64-bit Windows VST2 and VST3 plugins in a 64-bit +Linux VST host as if they were native VST2 and VST3 plugins, with optional +support for [plugin groups](#plugin-groups) to enable inter-plugin communication +for VST2 plugins and quick startup times. Its modern concurrent architecture and +focus on transparency allows yabridge to be both fast and highly compatible, +while also staying easy to debug and maintain. -VST3 support for yabridge is still very far removed from being in a usable -state, but you can track the progress in the -[feature/vst3](https://github.com/robbert-vdh/yabridge/tree/feature/vst3) -branch. +_VST3 support is currently experimental and only available on the master branch. Yabridge 3.0 will ship with full VST3 support._ +_See [this document](https://github.com/robbert-vdh/yabridge/blob/master/src/common/serialization/vst3/README.md) for all currently implemented interfaces._ ![yabridge screenshot](https://raw.githubusercontent.com/robbert-vdh/yabridge/master/screenshot.png) @@ -36,21 +35,24 @@ branch. - [Performance tuning](#performance-tuning) - [Runtime dependencies and known issues](#runtime-dependencies-and-known-issues) - [Building](#building) + - [Building without VST3 support](#building-without-vst3-support) - [32-bit bitbridge](#32-bit-bitbridge) - [Debugging](#debugging) - [Attaching a debugger](#attaching-a-debugger) ## Tested with -Yabridge has been tested under the following VST hosts using Wine Staging 5.9: +Yabridge has been tested under the following hosts using Wine Staging 6.0: -- Bitwig Studio 3.3 -- Carla 2.2 -- Ardour 6.5 -- Mixbus 6.0.702 -- Qtractor 0.9.18 -- REAPER 6.18 -- Renoise 3.2.4 +| Host | VST2 | VST3 | +| ----------------- | ------------------ | -------------------------------------------------------------------------------- | +| Bitwig Studio 3.3 | :heavy_check_mark: | :heavy_check_mark: | +| Carla 2.2 | :heavy_check_mark: | :heavy_check_mark: | +| REAPER 6.19 | :heavy_check_mark: | :heavy_check_mark: | +| Ardour 6.5 | :heavy_check_mark: | :warning: Several plugins segfault because Ardour skips part of the setup proces | +| Mixbus 6.0.702 | :heavy_check_mark: | :warning: Same situation as with Ardour | +| Qtractor 0.9.19 | :heavy_check_mark: | :x: See [rncbc/qtractor#291](https://github.com/rncbc/qtractor/issues/291) | +| Renoise 3.2.4 | :heavy_check_mark: | Does not support VST3 | Please let me know if there are any issues with other VST hosts. @@ -74,10 +76,8 @@ Linux Mint and Pop!\_OS should install Wine Staging from the [WineHQ repositories](https://wiki.winehq.org/Download) as the versions of Wine provided by those distro's repositories will be too old to be used with yabridge. -Most VST plugins first need to be installed in your Wine environment before -they can be converted by yabridge for use in linux. For a general overview -on how to use Wine to install Windows applications, check out Wine's -[user guide](https://wiki.winehq.org/Wine_User%27s_Guide#Using_Wine). +For a general overview on how to use Wine to install Windows applications, check +out Wine's [user guide](https://wiki.winehq.org/Wine_User%27s_Guide#Using_Wine). ### Automatic setup (recommended) @@ -103,51 +103,61 @@ yabridge from source or if you installed the files to some other location, then you can use `yabridgectl set --path=` to tell yabridgectl where it can find the files. -Next, you'll want to tell yabridgectl where it can find your plugins. For this -you can use yabridgectl's `add`, `rm` and `list` commands. For instance, to add -the most common VST2 plugin directory, use `yabridgectl add "$HOME/.wine/drive_c/Program Files/Steinberg/VstPlugins"`. You can use +Next, you'll want to tell yabridgectl where it can find your VST2 and VST3 +plugins. **Note that VST3 support is not yet available on yabridge 2.x.** For +this you can use yabridgectl's `add`, `rm` and `list` commands. You can also use `yabridgectl status` to get an overview of the current settings and the -installation status of all of your plugins. +installation status of all of your plugins. To add the most common VST2 plugin +directory, use +`yabridgectl add "$HOME/.wine/drive_c/Program Files/Steinberg/VstPlugins"`. VST3 +plugins under Windows are always installed to the same directory, and you can +use `yabridgectl add "$HOME/.wine/drive_c/Program Files/Common Files/VST3"` to +add that one. Finally, you can run `yabridgectl sync` to finish setting up yabridge for all of -your plugins. Simply tell your VST host to search for plugins in the directories -you've just added using `yabridgectl add` and you'll be good to go. _Don't -forget to rerun `yabridgectl sync` whenever you update yabridge if you are using -the default copy-based installation method._ +your plugins. For VST2 plugins this will create `.so` files alongside the +Windows VST2 plugins, so if you tell your Linux VST host to search for VST2 +plugins there you'll be good to go. VST3 plugins are always set up in +`~/.vst3/yabridge` as per the VST3 specification, and your VST3 host will pick +those up automatically. _Don't forget to rerun `yabridgectl sync` whenever you +update yabridge if you are using the default copy-based installation method._ ### Manual setup Setting up yabridge through yabridgectl is the recommended installation method as it makes updating easier and yabridgectl will check for some common mistakes -during the installation process. To set up yabridge without using yabridgectl, -first download and extract yabridge's files like in the section above. The rest -of this section assumes that you have extracted the files to `~/.local/share` -(such that `~/.local/share/yabridge/libyabridge.so` exists), and that you want -to set up yabridge for the VST2 plugin called `~/.wine/drive_c/Program Files/Steinberg/VstPlugins/plugin.dll`. +during the installation process. To manually set up yabridge for VST2 plugins, first +download and extract yabridge's files like in the section above. The rest of +this section assumes that you have extracted the files to `~/.local/share` (such +that `~/.local/share/yabridge/libyabridge-vst2.so` exists), and that you want to +set up yabridge for the VST2 plugin called +`~/.wine/drive_c/Program Files/Steinberg/VstPlugins/plugin.dll`. Depending on whether you want to use copy or symlink-based installation method, you can then set up yabridge for that plugin by creating a copy or symlink of -`libyabridge.so` next to `plugin.dll` called `plugin.so`. For the example, you -can use either: +`libyabridge-vst2.so` next to `plugin.dll` called `plugin.so`. For the example, +you can use either: ```shell # For the copy-based installation method -cp ~/.local/share/yabridge/libyabridge.so "$HOME/.wine/drive_c/Program Files/Steinberg/VstPlugins/plugin.so" +cp ~/.local/share/yabridge/libyabridge-vst2.so "$HOME/.wine/drive_c/Program Files/Steinberg/VstPlugins/plugin.so" # For the symlink-based installation method -ln -sf ~/.local/share/yabridge/libyabridge.so "$HOME/.wine/drive_c/Program Files/Steinberg/VstPlugins/plugin.so" +ln -sf ~/.local/share/yabridge/libyabridge-vst2.so "$HOME/.wine/drive_c/Program Files/Steinberg/VstPlugins/plugin.so" ``` -The symlink-based installation method will not work with any host that does not -individually sandbox its plugins. If you are using the copy-based installation -method, then don't forget to overwrite all copies of `libyabridge.so` you -created this way whenever you update yabridge. +Doing the same thing for VST3 plugins involves creating a [merged VST3 +bundle](https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html#mergedbundles) +by hand with the Windows VST3 plugin symlinked in. Doing this without +yabridgectl is not supported since it's a very error prone process. ### DAW setup -Finally, open your DAW's VST location configuration and tell it to look for -plugins under `~/.wine/drive_c/Program Files/Steinberg/VstPlugins`, or whichever -directories you've added in yabridgectl. That way it will automatically pick up -all of your Windows VST2 plugins. +After first setting up yabridge for VST2 plugins, open your DAW's plugin location +configuration and tell it to search for VST2 plugins under +`~/.wine/drive_c/Program Files/Steinberg/VstPlugins`, or whichever directories +you've added in yabridgectl. That way it will automatically pick up all of your +Windows VST2 plugins. For VST3 plugins no additional DAW configuration is +needed, as those plugins will be set up under `~/.vst3/yabridge`. ### Bitbridge @@ -158,6 +168,10 @@ 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. +_Because of the way VST3 bundles work, it's at the moment not possible to choose +between the 32-bit and 64-bit versions of a VST3 plugin if you have both +installed. We'll add a `yabridge.toml` option for this later._ + ### Wine prefixes It is also possible to use yabridge with multiple Wine prefixes. Yabridge will @@ -169,8 +183,8 @@ the Wine prefix for all instances of yabridge. This section is only relevant if you're using the _copy-based_ installation method and your yabridge files are located somewhere other than in -`~/.local/share/yabridge`. If you're using one of the AUR packages then you can -also skip this section. +`~/.local/share/yabridge`. You can likely skip this section. If you're using one +of the AUR packages then you also don't have to worry about any of this. Yabridge needs to know where it can find `yabridge-host.exe`. By default yabridge will search your through search path as well as in @@ -253,12 +267,19 @@ process. Of course, plugin groups with the same name but in different Wine prefixes and with different architectures will be run independently of each other. See below for an [example](#example) of how these groups can be set up. +_Note that because of the way VST3 works, multiple instances of a single VST3 +plugin will always be hosted in a single process regardless of whether you have +enabled plugin groups or not. The only reason to use plugin groups with VST3 +plugins is to get slightly lower loading times the first time you load a new +plugin._ + #### Compatibility options -| Option | Values | Description | -| --------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `cache_time_info` | `{true,false}` | Compatibility option for plugins that call `audioMasterGetTime()` multiple times during a single processing cycle. With this option subsequent calls during a single audio processing cycle will reuse the value returned by the first call to this function. This is a bug in the plugin, and this option serves as a temporary workaround until the plugin fixes the issue. | -| `editor_double_embed` | `{true,false}` | Compatibility option for plugins that rely on the absolute screen coordinates of the window they're embedded in. Since the Wine window gets embedded inside of a window provided by your DAW, these coordinates won't match up and the plugin would end up drawing in the wrong location without this option. Currently the only known plugins that require this option are _PSPaudioware_ plugins with expandable GUIs, such as E27. Defaults to `false`. | +| Option | Values | Description | +| --------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cache_time_info` | `{true,false}` | Compatibility option for plugins that call `audioMasterGetTime()` multiple times during a single processing cycle. With this option subsequent calls during a single audio processing cycle will reuse the value returned by the first call to this function. This is a bug in the plugin, and this option serves as a temporary workaround until the plugin fixes the issue. | +| `editor_double_embed` | `{true,false}` | Compatibility option for plugins that rely on the absolute screen coordinates of the window they're embedded in. Since the Wine window gets embedded inside of a window provided by your DAW, these coordinates won't match up and the plugin would end up drawing in the wrong location without this option. Currently the only known plugins that require this option are _PSPaudioware_ plugins with expandable GUIs, such as E27. Defaults to `false`. | +| `editor_xembed` | `{true,false}` | Use Wine's XEmbed implementation instead of yabridge's normal window embedding method. Some plugins will have redrawing issues when using XEmbed and editor resizing won't always work properly with it, but it could be useful in certain setups. You may need to use [this Wine patch](https://github.com/psycha0s/airwave/blob/master/fix-xembed-wine-windows.patch) if you're getting blank editor windows. Defaults to `false`. _This option is only availble on the master branch._ | These options are workarounds for issues mentioned in the [known issues](#runtime-dependencies-and-known-issues) section. Depending on the hosts @@ -266,18 +287,12 @@ and plugins you use you might want to enable some of them. #### Example -All of the paths used here are relative to the `yabridge.toml` file. +All of the paths used here are relative to the `yabridge.toml` file. A +configuration file for VST2 plugins might look a little something like this: ```toml # ~/.wine/drive_c/Program Files/Steinberg/VstPlugins/yabridge.toml -# This would cause all plugins to be hosted within a single process. Doing so -# greatly reduces the loading time of individual plugins, with the caveat being -# that plugins are no longer sandboxed from eachother. -# -# ["*"] -# group = "all" - ["FabFilter Pro-Q 3.so"] group = "fabfilter" @@ -292,6 +307,9 @@ group = "toneboosters" ["PSPaudioware"] editor_double_embed = true +["Analog Lab 3.so"] +editor_xembed = true + ["SWAM Cello 64bit.so"] cache_time_info = true @@ -307,6 +325,28 @@ group = "This will be ignored!" # Of course, you can also add multiple plugins to the same group by hand ["iZotope7/Insight 2.so"] group = "izotope" + +# This would cause all plugins to be hosted within a single process. Doing so +# greatly reduces the loading time of individual plugins, with the caveat being +# that plugins are no longer sandboxed from eachother. +# +# ["*"] +# group = "all" +``` + +For VST3 plugins you should just match the directory instead of the `.so` file +deep within in, like this: + +```toml +# ~/.vst3/yabridge/yabridge.toml + +["FabFilter*.vst3"] +group = "fabfilter" + +["Misstortion2.vst3"] +# This option is not needed and also not recommended, but an example config file +# without any options looks weird +editor_xembed = true ``` ## Troubleshooting common issues @@ -389,7 +429,7 @@ these negative side effects: - First of all, you'll want to make sure that you can run programs with realtime priorities. Note that on Arch and Manjaro this does not necessarily require a - realtime kernel as they include the `PREMPT` patch set in their regular + realtime kernel as they include the `PREEMPT` patch set in their regular kernels. You can verify that this is workign correctly by running `chrt -f 10 whoami`, which should print your username. @@ -421,15 +461,16 @@ these negative side effects: other distros, then please let me know! - [Plugin groups](#plugin-groups) can also greatly improve performance when - using many instances of the same plugin. Some plugins, like the BBC Spitfire + using many instances of the same VST2 plugin. _VST3 plugins have similar + functionality built in by design_. Some plugins, like the BBC Spitfire plugins, can share a lot of resources between different instances of the plugin. Hosting all instances of the same plugin in a single process can in those cases greatly reduce overall CPU usage and get rid of latency spikes. ## Runtime dependencies and known issues -Any VST2 plugin should function out of the box, although some plugins will need -some additional dependencies for their GUIs to work correctly. Notable examples +Any plugin should function out of the box, although some plugins will need some +additional dependencies for their GUIs to work correctly. Notable examples include: - **Serum** requires you to disable `d2d1.dll` in `winecfg` and to install @@ -515,9 +556,12 @@ the following dependencies: The following dependencies are included in the repository as a Meson wrap: -- bitsery -- function2 -- tomlplusplus +- [bitsery](https://github.com/fraillt/bitsery) +- [function2](https://github.com/Naios/function2) +- [tomlplusplus](https://github.com/marzer/tomlplusplus) +- Version 3.7.1 of the [VST3 SDK](https://github.com/robbert-vdh/vst3sdk) with + some [patches](https://github.com/robbert-vdh/yabridge/blob/master/tools/patch-vst3-sdk.sh) + to allow Winelib compilation The project can then be compiled as follows: @@ -544,7 +588,7 @@ After you've finished building you can follow the instructions under the 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 +act as a bitbridge, 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 versions of the Boost and XCB libraries. This can then be set up as follows: @@ -612,7 +656,7 @@ launch winedbg in a seperate detached terminal emulator so it doesn't terminate together with the plugin, and winedbg can be a bit picky about the arguments it accepts. I've already set this up behind a feature flag for use in KDE Plasma. Other desktop environments and window managers will require some slight -modifications in `src/plugin/plugin-bridge.cpp`. To enable this, simply run: +modifications in `src/plugin/host-process.cpp`. To enable this, simply run: ```shell meson configure build --buildtype=debug -Dwith-winedbg=true diff --git a/docs/architecture.md b/docs/architecture.md index 78998dec..ebfe6c29 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,9 +1,11 @@ # Architecture +TODO: This document has not yet been updated since adding VST3 support + 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). +`yabridge-host-32.exe`/`yabridge-host-32.exe.so` if the bitbridge 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 diff --git a/docs/vst3.md b/docs/vst3.md new file mode 100644 index 00000000..2398bd61 --- /dev/null +++ b/docs/vst3.md @@ -0,0 +1,129 @@ +# VST3 serialization + +TODO: Flesh this out further, update the instantiation part, make the proxying part clearer + +TODO: Link to `src/common/serialization/vst3/README.md` + +The VST3 SDK uses an architecture where every concrete object inherits from an +interface, and every interface inherits from `FUnknown`. `FUnkonwn` offers a +dynamic casting interface through `queryInterface()` and a reference counting +mechanism that calls `delete this;` when the reference count reaches 0. Every +interface gets a unique identifier. It then uses a smart pointer system +(`FUnknownPtr`) that queries whether the `FUnknown` matches a certain +interface by checking whether the IDs match up, allowing casts to that interface +if the `FUnkonwn` matches. Those smart pointers also use that reference counting +mechanism to destroy the object when the last pointer gets dropped. + +Another important part of this system is interface versioning. Old interfaces +cannot be changed, so when the SDK adds new functionality to an existing +interface it defines a new interface that inherits from the old one. The +`queryInterface()` implementation should then allow casts to all of the +implemented interface versions. + +Lastly, the interfaces provide both getters for static, non-chancing data (such +as the classes registered in a plugin factory) as well as functions that perform +side effects or return dynamically changing data (such as the input/output +configuration for an audio processor). + +Yabridge's serialization and communication model for VST3 is thus a lot more +complicated than for VST2 since all of these objects are loosely coupled and are +instantiated and managed by the host. The basic model works as follows: + +1. The main idea behind yabridge's VST3 implementation is that we define + monolithic proxy objects that can proxy any object created by the Windows + VST3 plugin. These proxy objects indirectly inherit from all applicable + interfaces defiend in the VST3 SDK. `Vst3PluginProxy` implements all + interfaces that can be implemented by plugins, and `Vst3HostProxy` implements + all interfaces that are to be implemented by the host. + + TODO: Find out if `Vst3HostProxy` is needed, or if objects provided by the + host never implement multiple interfaces (which I think might be the case) + +2. For every interface `IFoo`, we provide an abstract implementation called + `YaFoo`. This implementation mostly contain message object we use to make + specific function calls on the actual objects we are proxying. The + implementation also comes with a function that takes an `FUnknown` pointer, + checks whether the object behind that pointer supports `IFoo`, and then + stores the result along with any potential static payload data as a + `YaFoo::ConstrctArgs` object. +3. Proxy object are instantiated while handling + `IPluginFactory::createInstance()` for `Vst3PluginProxy`, and during + `IPluginBase::initialize()` and `IPluginFactory::setHostContext()` for + `Vst3HostProxy` (TODO: Same here). On the receiving side of those functions + (where we call the actual function implemented by the plugin or the host), we + receive an `IPtr` smart pointer to an object provided by the host or the + plugin. We use this object to iterate over every applicable `YaFoo` as + mentioend above. All of these `YaFoo::ConstructArgs` objects along with a + unique identifier for this specific object are then serialized and + transmitted to the other side. With this information we can create a proxy + object that supports all the same interfaces (and thus allows calls to the + functions in those interfaces) as the original object we are proxying. +4. As mentioend, every object we instantiate gets assigned a unique identifier. + When dealign with objects created by the Windows VST3 plugin, the object's + `FUnknown` pointer will be stored in an `std::map` map. + This way we can refer to it later on when we receive a request to call a + specific function on the plugin. +5. If `IFoo` is a versioned interface such as `IPluginFactory{,2,3}`, the + creation of `YaFoo::ConstrctArgs` and the definition of `YaFoo`'s query + interface work slightly differently. When copying the data for a plugin + factory, we'll start copying from `IPluginFactory`, and we'll copy data from + each newer version of the interface that the `IPtr` supports. + During this process we keep track of which interfaces were supported by the + native plugin. In our query interface method we then only report support for + the same interface versions that were supported by the original + `IPtr` we are proxying. + +## Interface Instantiation + +Creating a new instance of an interface using the plugin factory wroks as +follows. This describes the object lifecycle. The actual serialization and +proxying is described in the section above. + +1. The host calls `createInterface(cid, _iid, obj)` on an `IPluginFactory` + implementation exposed to the host as described above. +2. We check which interface we support matches the `_iid`. If we don't support + the interface, we'll log a message about it and return that we do not support + the itnerface. +3. If we determine that `_iid` matches `IFoo`, then we'll send a + `YaFoo::Construct{cid}` to the Wine plugin host process. +4. The Wine plugin host will then call + `module->getFactory().createInstance(cid)` using the Windows VST3 + plugin's plugin factory to ask it to create an instance of that interface. If + this operation fails and returns a null pointer, we'll send a + `kNotImplemented` result code back to indicate that the instantiation was not + successful and we relay this on the plugin side. +5. As mentioned above, we will generate a unique instance identifier for the + newly generated object so we can refer to it later. We then serialize that + identifier along with what other static data is available in `IFoo` in a + `YaFoo::ConstructArgs` object. +6. We then move `IPtr` to an `std::map>` with that + unique identifier we generated earlier as a key so we can refer to it later + in later function calls. +7. On the plugin side we can now use the `YaFoo::Arguments` object we received + to create a `YaFooPluginImpl` object that can send control messages to the + Wine plugin host. +8. Finally a pointer to this `YaFooPluginImpl` gets returned as the last step of + the initialization process. + +## Simple objects + +For serializing objects of interfaces that purely contain getters and setters +(and thus don't need to perform any host callbacks), we'll simply have a +constructor that takes the `IFoo` by `IPtr` or reference (depending on how it's +used in the SDK) and reads the data from it to create a serializable copy of +that object. + +## Safety notes + +- None of the destructors in the interfaces defined by the SDK are marked as + virtual because this could apparently [break binary + compatibility](https://github.com/steinbergmedia/vst3sdk/issues/21). This + means that the destructor of the class that implemented `release()` will be + called. This is something to keep in mind when dealing with inheritence. +- Since everything behind the scenes makes use of these `addRef()` and + `release()` reference counting functions, we can't use the standard library's + smart pointers when dealing with objects that are shared with the host or with + the Windows VST3 plugin. In `IPtr`'s destructor it will call release, and + the objects will clean themselfs up with a `delete this;` when the reference + count reaches 0. Combining this with the STL cmart pointers this would result + in a double free. diff --git a/meson.build b/meson.build index f045d95d..4253245a 100644 --- a/meson.build +++ b/meson.build @@ -2,7 +2,7 @@ project( 'yabridge', 'cpp', version : '2.2.1', - default_options : ['warning_level=3', 'cpp_std=c++2a', 'build.cpp_std=c++2a'] + default_options : ['warning_level=3', 'cpp_std=c++2a', 'build.cpp_std=c++2a'], ) # Meson does not let us set a default cross compiler, which makes sense, but it @@ -31,12 +31,13 @@ compiler_options = [ '-fvisibility-inlines-hidden', # Disable the use of concepts in Boost.Asio until Boost 1.73 gets released # https://github.com/boostorg/asio/issues/312 - '-DBOOST_ASIO_DISABLE_CONCEPTS' + '-DBOOST_ASIO_DISABLE_CONCEPTS', ] with_bitbridge = get_option('with-bitbridge') with_static_boost = get_option('with-static-boost') with_winedbg = get_option('with-winedbg') +with_vst3 = get_option('with-vst3') if with_bitbridge compiler_options += '-DWITH_BITBRIDGE' @@ -48,6 +49,10 @@ if with_winedbg compiler_options += '-DWITH_WINEDBG' endif +if with_vst3 + compiler_options += '-DWITH_VST3' +endif + # Wine versions after Wine 5.6 and before 6.0 require a `__cdecl` calling # convention to be specified on the `main()` functions or else `argc` and `argv` # will point to the wrong memory. Similarly, with other versions of Wine this @@ -66,63 +71,76 @@ endif # and the name of the host binary subdir('src/common/config') -# 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 : with_static_boost) -boost_filesystem_dep = dependency( - 'boost', - version : '>=1.66', - modules : ['filesystem'], - static : with_static_boost -) -bitsery_dep = subproject('bitsery', version : '5.2.0').get_variable('bitsery_dep') -function2_dep = subproject('function2', version : '4.1.0').get_variable('function2_dep') -threads_dep = dependency('threads') -tomlplusplus_dep = subproject('tomlplusplus', version : '2.1.0').get_variable('tomlplusplus_dep') -# The built in threads dependency does not know how to handle winegcc -wine_threads_dep = declare_dependency(link_args : '-lpthread') -xcb_dep = dependency('xcb') +vst2_plugin_sources = [ + 'src/common/communication/common.cpp', + 'src/common/communication/vst2.cpp', + 'src/common/serialization/vst2.cpp', + 'src/common/configuration.cpp', + 'src/common/logging/common.cpp', + 'src/common/logging/vst2.cpp', + 'src/common/plugins.cpp', + 'src/common/utils.cpp', + 'src/plugin/bridges/vst2.cpp', + 'src/plugin/host-process.cpp', + 'src/plugin/utils.cpp', + 'src/plugin/vst2-plugin.cpp', + version_header, +] -include_dir = include_directories('src/include') - -# The application consists of a VST plugin (yabridge) that calls a winelib -# program (yabridge-host) that can host Windows VST plugins. More information -# about the way these two components work together can be found in the readme -# file. - -shared_library( - 'yabridge', - [ - 'src/common/configuration.cpp', - 'src/common/logging.cpp', - 'src/common/serialization.cpp', - 'src/common/communication.cpp', - 'src/common/utils.cpp', - 'src/plugin/host-process.cpp', - 'src/plugin/plugin.cpp', - 'src/plugin/plugin-bridge.cpp', - 'src/plugin/utils.cpp', - version_header, - ], - native : true, - include_directories : include_dir, - dependencies : [ - boost_dep, - boost_filesystem_dep, - bitsery_dep, - threads_dep, - tomlplusplus_dep - ], - cpp_args : compiler_options, - link_args : ['-ldl'] -) +vst3_plugin_sources = [ + 'src/common/communication/common.cpp', + 'src/common/logging/common.cpp', + 'src/common/logging/vst3.cpp', + 'src/common/serialization/vst3/plug-view/plug-view.cpp', + 'src/common/serialization/vst3/plug-frame/plug-frame.cpp', + 'src/common/serialization/vst3/plugin/audio-processor.cpp', + 'src/common/serialization/vst3/plugin/component.cpp', + 'src/common/serialization/vst3/plugin/connection-point.cpp', + 'src/common/serialization/vst3/plugin/edit-controller.cpp', + 'src/common/serialization/vst3/plugin/edit-controller-2.cpp', + 'src/common/serialization/vst3/plugin/plugin-base.cpp', + 'src/common/serialization/vst3/plugin/program-list-data.cpp', + 'src/common/serialization/vst3/plugin/unit-data.cpp', + 'src/common/serialization/vst3/plugin/unit-info.cpp', + 'src/common/serialization/vst3/component-handler/component-handler.cpp', + 'src/common/serialization/vst3/component-handler/unit-handler.cpp', + 'src/common/serialization/vst3/host-context/host-application.cpp', + 'src/common/serialization/vst3/attribute-list.cpp', + 'src/common/serialization/vst3/base.cpp', + 'src/common/serialization/vst3/component-handler-proxy.cpp', + 'src/common/serialization/vst3/connection-point-proxy.cpp', + 'src/common/serialization/vst3/event-list.cpp', + 'src/common/serialization/vst3/host-context-proxy.cpp', + 'src/common/serialization/vst3/message.cpp', + 'src/common/serialization/vst3/param-value-queue.cpp', + 'src/common/serialization/vst3/parameter-changes.cpp', + 'src/common/serialization/vst3/plug-frame-proxy.cpp', + 'src/common/serialization/vst3/plug-view-proxy.cpp', + 'src/common/serialization/vst3/plugin-proxy.cpp', + 'src/common/serialization/vst3/plugin-factory.cpp', + 'src/common/serialization/vst3/process-data.cpp', + 'src/common/configuration.cpp', + 'src/common/plugins.cpp', + 'src/common/utils.cpp', + 'src/plugin/bridges/vst3.cpp', + 'src/plugin/bridges/vst3-impls/plugin-factory.cpp', + 'src/plugin/bridges/vst3-impls/plug-view-proxy.cpp', + 'src/plugin/bridges/vst3-impls/plugin-proxy.cpp', + 'src/plugin/host-process.cpp', + 'src/plugin/utils.cpp', + 'src/plugin/vst3-plugin.cpp', + version_header, +] host_sources = [ + 'src/common/communication/vst2.cpp', + 'src/common/serialization/vst2.cpp', 'src/common/configuration.cpp', - 'src/common/logging.cpp', - 'src/common/serialization.cpp', - 'src/common/communication.cpp', + 'src/common/logging/common.cpp', + 'src/common/logging/vst2.cpp', + 'src/common/plugins.cpp', 'src/common/utils.cpp', + 'src/wine-host/bridges/common.cpp', 'src/wine-host/bridges/vst2.cpp', 'src/wine-host/editor.cpp', 'src/wine-host/editor.cpp', @@ -130,47 +148,266 @@ host_sources = [ version_header, ] +if with_vst3 + host_sources += [ + 'src/common/logging/vst3.cpp', + 'src/common/serialization/vst3/plug-view/plug-view.cpp', + 'src/common/serialization/vst3/plug-frame/plug-frame.cpp', + 'src/common/serialization/vst3/plugin/audio-processor.cpp', + 'src/common/serialization/vst3/plugin/component.cpp', + 'src/common/serialization/vst3/plugin/connection-point.cpp', + 'src/common/serialization/vst3/plugin/edit-controller.cpp', + 'src/common/serialization/vst3/plugin/edit-controller-2.cpp', + 'src/common/serialization/vst3/plugin/plugin-base.cpp', + 'src/common/serialization/vst3/plugin/program-list-data.cpp', + 'src/common/serialization/vst3/plugin/unit-data.cpp', + 'src/common/serialization/vst3/plugin/unit-info.cpp', + 'src/common/serialization/vst3/component-handler/component-handler.cpp', + 'src/common/serialization/vst3/component-handler/unit-handler.cpp', + 'src/common/serialization/vst3/host-context/host-application.cpp', + 'src/common/serialization/vst3/attribute-list.cpp', + 'src/common/serialization/vst3/base.cpp', + 'src/common/serialization/vst3/component-handler-proxy.cpp', + 'src/common/serialization/vst3/connection-point-proxy.cpp', + 'src/common/serialization/vst3/event-list.cpp', + 'src/common/serialization/vst3/host-context-proxy.cpp', + 'src/common/serialization/vst3/message.cpp', + 'src/common/serialization/vst3/param-value-queue.cpp', + 'src/common/serialization/vst3/parameter-changes.cpp', + 'src/common/serialization/vst3/plug-frame-proxy.cpp', + 'src/common/serialization/vst3/plug-view-proxy.cpp', + 'src/common/serialization/vst3/plugin-proxy.cpp', + 'src/common/serialization/vst3/plugin-factory.cpp', + 'src/common/serialization/vst3/process-data.cpp', + 'src/wine-host/bridges/vst3-impls/component-handler-proxy.cpp', + 'src/wine-host/bridges/vst3-impls/connection-point-proxy.cpp', + 'src/wine-host/bridges/vst3-impls/host-context-proxy.cpp', + 'src/wine-host/bridges/vst3-impls/plug-frame-proxy.cpp', + 'src/wine-host/bridges/vst3.cpp', + ] +endif + 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( - individual_host_name_64bit, - individual_host_sources, - native : false, +# 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 : with_static_boost) +boost_filesystem_dep = dependency( + 'boost', + version : '>=1.66', + modules : ['filesystem'], + static : with_static_boost, +) +bitsery_dep = subproject('bitsery', version : '5.2.0').get_variable('bitsery_dep') +function2_dep = subproject('function2', version : '4.1.0').get_variable('function2_dep') +threads_dep = dependency('threads') +tomlplusplus_dep = subproject('tomlplusplus', version : '2.1.0').get_variable('tomlplusplus_dep') +wine_gdi32_dep = declare_dependency(link_args : '-lgdi32') +# The built in threads dependency does not know how to handle winegcc +wine_threads_dep = declare_dependency(link_args : '-lpthread') +xcb_dep = dependency('xcb') + +wine_ole32_dep = declare_dependency(link_args : '-lole32') +# The SDK includes a comment pragma that would link to this on MSVC +wine_shell32_dep = declare_dependency(link_args : '-lshell32') +wine_uuid_dep = declare_dependency(link_args : '-luuid') + +include_dir = include_directories('src/include') + +if with_vst3 + # Meson does not allow mixing native and non native dependencies from + # subprojects. The only workaround is to only define the necessary variables + # there, and to then assemble the dependencies here ourselves. + vst3 = subproject('vst3', version : '3.7.1') + vst3_compiler_options = vst3.get_variable('compiler_options') + vst3_include_dir = vst3.get_variable('include_dir') + + # We'll create a dependency for the plugin SDK for our native VST3 plugin + vst3_base_native = static_library( + 'base_native', + vst3.get_variable('base_sources'), + cpp_args : vst3_compiler_options + ['-Wno-cpp'], + include_directories : vst3_include_dir, + override_options : ['warning_level=0'], + native : true, + ) + vst3_pluginterfaces_native = static_library( + 'pluginterfaces_native', + vst3.get_variable('pluginterfaces_sources'), + cpp_args : vst3_compiler_options, + include_directories : vst3_include_dir, + override_options : ['warning_level=0'], + native : true, + ) + vst3_sdk_native = static_library( + 'sdk_native', + vst3.get_variable('sdk_common_sources') + vst3.get_variable('sdk_sources'), + link_with : [vst3_base_native, vst3_pluginterfaces_native], + cpp_args : vst3_compiler_options + ['-Wno-multichar'], + include_directories : vst3_include_dir, + override_options : ['warning_level=0'], + native : true, + ) + vst3_sdk_native_dep = declare_dependency( + link_with : vst3_sdk_native, + include_directories : vst3_include_dir, + compile_args : vst3_compiler_options, + ) + + # And another dependency for the host SDK for our Wine host applications + # We need to do some minor hacking to get this to compile with winegcc. Most + # notably some attributes are named differently, and the SDK uses 'Windows.h' + # instead of 'windows.h' like how the file is actually called. + # message(vst3_include_dir) + vst3_sdk_base_dir = vst3.get_variable('sdk_base_dir') + patch_result = run_command('tools/patch-vst3-sdk.sh', vst3_sdk_base_dir) + if patch_result.returncode() == 0 + message(patch_result.stdout()) + else + error('Error while trying to patch the VST3 SDK:\n' + patch_result.stderr()) + endif + + vst3_wine_compiler_options = [ + # Some stuff from `windows.h` results in conflicting definitions + '-DNOMINMAX', + '-DWINE_NOWINSOCK', + ] + vst3_base_wine_64bit = static_library( + 'vst3_base_wine_64bit', + vst3.get_variable('base_sources'), + cpp_args : vst3_compiler_options + vst3_wine_compiler_options + ['-m64', '-Wno-cpp'], + include_directories : vst3_include_dir, + override_options : ['warning_level=0'], + native : false, + ) + vst3_pluginterfaces_wine_64bit = static_library( + 'vst3_pluginterfaces_wine_64bit', + vst3.get_variable('pluginterfaces_sources'), + cpp_args : vst3_compiler_options + vst3_wine_compiler_options + ['-m64'], + include_directories : vst3_include_dir, + override_options : ['warning_level=0'], + native : false, + ) + vst3_sdk_hosting_wine_64bit = static_library( + 'vst3_sdk_hosting_wine_64bit', + vst3.get_variable('sdk_common_sources') + vst3.get_variable('sdk_hosting_sources'), + link_with : [vst3_base_wine_64bit, vst3_pluginterfaces_wine_64bit], + cpp_args : vst3_compiler_options + vst3_wine_compiler_options + ['-m64', '-Wno-multichar'], + include_directories : vst3_include_dir, + override_options : ['warning_level=0'], + native : false, + ) + vst3_sdk_hosting_wine_64bit_dep = declare_dependency( + link_with : vst3_sdk_hosting_wine_64bit, + include_directories : vst3_include_dir, + # This does mean that we now have a lot of defines in our code, but the + # alternative would be patching every location in the SDK where they include + # `windows.h` + compile_args : vst3_compiler_options + vst3_wine_compiler_options, + ) + + # And another time for the 32-bit version + if with_bitbridge + vst3_base_wine_32bit = static_library( + 'vst3_base_wine_32bit', + vst3.get_variable('base_sources'), + cpp_args : vst3_compiler_options + vst3_wine_compiler_options + ['-m32', '-Wno-cpp'], + include_directories : vst3_include_dir, + override_options : ['warning_level=0'], + native : false, + ) + vst3_pluginterfaces_wine_32bit = static_library( + 'vst3_pluginterfaces_wine_32bit', + vst3.get_variable('pluginterfaces_sources'), + cpp_args : vst3_compiler_options + vst3_wine_compiler_options + ['-m32'], + include_directories : vst3_include_dir, + override_options : ['warning_level=0'], + native : false, + ) + vst3_sdk_hosting_wine_32bit = static_library( + 'vst3_sdk_hosting_wine_32bit', + vst3.get_variable('sdk_common_sources') + vst3.get_variable('sdk_hosting_sources'), + link_with : [vst3_base_wine_32bit, vst3_pluginterfaces_wine_32bit], + cpp_args : vst3_compiler_options + vst3_wine_compiler_options + ['-m32', '-Wno-multichar'], + include_directories : vst3_include_dir, + override_options : ['warning_level=0'], + native : false, + ) + vst3_sdk_hosting_wine_32bit_dep = declare_dependency( + link_with : vst3_sdk_hosting_wine_32bit, + include_directories : vst3_include_dir, + # This does mean that we now have a lot of defines in our code, but the + # alternative would be patching every location in the SDK where they include + # `windows.h` + compile_args : vst3_compiler_options + vst3_wine_compiler_options, + ) + endif +endif + +# The application consists of a plugin (`libyabridge-{vst2,vst3}.so`) that calls +# a Winelib application (`yabridge-{host,group}{,-32}.exe`) that can host +# Windows VST plugins. More information about the way these two components work +# together can be found in `docs/architecture.md`. + +shared_library( + 'yabridge-vst2', + vst2_plugin_sources, + native : true, include_directories : include_dir, dependencies : [ boost_dep, boost_filesystem_dep, bitsery_dep, - function2_dep, + threads_dep, tomlplusplus_dep, - wine_threads_dep, - xcb_dep ], - cpp_args : compiler_options + ['-m64'], - link_args : ['-m64'] + cpp_args : compiler_options, + link_args : ['-ldl'] ) -executable( - group_host_name_64bit, - group_host_sources, - native : false, - include_directories : include_dir, - dependencies : [ - boost_dep, - boost_filesystem_dep, - bitsery_dep, - function2_dep, - tomlplusplus_dep, - wine_threads_dep, - xcb_dep - ], - cpp_args : compiler_options + ['-m64'], - link_args : ['-m64'] -) +if with_vst3 + # This is the VST3 equivalent of `libyabridge-vst2.so`. The Wine host + # applications can handle both VST2 and VST3 plugins. + shared_library( + 'yabridge-vst3', + vst3_plugin_sources, + native : true, + include_directories : include_dir, + dependencies : [ + boost_dep, + boost_filesystem_dep, + bitsery_dep, + threads_dep, + tomlplusplus_dep, + vst3_sdk_native_dep, + ], + cpp_args : compiler_options, + link_args : ['-ldl'], + ) +endif + +host_64bit_deps = [ + boost_dep, + boost_filesystem_dep, + bitsery_dep, + function2_dep, + tomlplusplus_dep, + wine_gdi32_dep, + wine_threads_dep, + xcb_dep, +] +if with_vst3 + host_64bit_deps += [ + vst3_sdk_hosting_wine_64bit_dep, + wine_ole32_dep, + wine_shell32_dep, + wine_uuid_dep, + ] +endif if with_bitbridge message('Bitbridge enabled, configuring a 32-bit host application') @@ -181,7 +418,7 @@ if with_bitbridge # 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_32bit_dep = winegcc.find_library( 'boost_filesystem', static : with_static_boost, dirs : [ @@ -198,24 +435,57 @@ if with_bitbridge '/usr/lib', ] ) - xcb_dep = winegcc.find_library('xcb') + xcb_32bit_dep = winegcc.find_library('xcb') + host_32bit_deps = [ + boost_dep, + boost_filesystem_32bit_dep, + bitsery_dep, + function2_dep, + tomlplusplus_dep, + wine_gdi32_dep, + wine_threads_dep, + xcb_32bit_dep, + ] + if with_vst3 + host_32bit_deps += [ + vst3_sdk_hosting_wine_32bit_dep, + wine_ole32_dep, + wine_shell32_dep, + wine_uuid_dep, + ] + endif +endif + +executable( + individual_host_name_64bit, + individual_host_sources, + native : false, + include_directories : include_dir, + dependencies : host_64bit_deps, + cpp_args : compiler_options + ['-m64'], + link_args : ['-m64'], +) + +executable( + group_host_name_64bit, + group_host_sources, + native : false, + include_directories : include_dir, + dependencies : host_64bit_deps, + cpp_args : compiler_options + ['-m64'], + link_args : ['-m64'], +) + +if with_bitbridge executable( individual_host_name_32bit, individual_host_sources, native : false, include_directories : include_dir, - dependencies : [ - boost_dep, - boost_filesystem_dep, - bitsery_dep, - function2_dep, - tomlplusplus_dep, - wine_threads_dep, - xcb_dep - ], + dependencies : host_32bit_deps, cpp_args : compiler_options + ['-m32'], - link_args : ['-m32'] + link_args : ['-m32'], ) executable( @@ -223,16 +493,8 @@ if with_bitbridge group_host_sources, native : false, include_directories : include_dir, - dependencies : [ - boost_dep, - boost_filesystem_dep, - bitsery_dep, - function2_dep, - tomlplusplus_dep, - wine_threads_dep, - xcb_dep - ], + dependencies : host_32bit_deps, cpp_args : compiler_options + ['-m32'], - link_args : ['-m32'] + link_args : ['-m32'], ) endif diff --git a/meson_options.txt b/meson_options.txt index bca9126c..c42a9a40 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -12,6 +12,20 @@ option( description : 'Enable static linking for Boost. Needed when distributing the binaries to other systems.' ) +# NOTE: Right now this does not actually require CMake. Because of a limitation +# in Meson we can't use the original build definitions just yet. For the +# time being we have just written our own meson.build to replace the SDK's +# CMake project. Once Meson allows mixing native and cross compiled CMake +# subprojects the commit that added this notice can be reverted. +# +# https://github.com/mesonbuild/meson/issues/8043 +option( + 'with-vst3', + type : 'boolean', + value : true, + description : 'Whether to build the VST3 version of yabridge. This requires CMake to be installed.' +) + option( 'with-winedbg', type : 'boolean', diff --git a/src/common/bitsery/ext/boost-path.h b/src/common/bitsery/ext/boost-path.h index fe149bc2..ba103324 100644 --- a/src/common/bitsery/ext/boost-path.h +++ b/src/common/bitsery/ext/boost-path.h @@ -27,8 +27,6 @@ namespace bitsery { namespace ext { -// TODO: There's probably a better way to do all of this - /** * An adapter for serializing and deserializing filesystem paths since they're * not mutable. diff --git a/src/common/bitsery/ext/vst3.h b/src/common/bitsery/ext/vst3.h new file mode 100644 index 00000000..458c66cd --- /dev/null +++ b/src/common/bitsery/ext/vst3.h @@ -0,0 +1,63 @@ +// 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 . + +#pragma once + +#include + +#include +#include +#include + +namespace bitsery { +namespace ext { + +/** + * An adapter for serializing and deserializing `Steinberg::FUID`s. + */ +class FUID { + public: + template + void serialize(Ser& ser, const Steinberg::FUID& uid, Fnc&&) const { + Steinberg::FUID::String uid_str; + uid.toString(uid_str); + + ser.text1b(uid_str); + } + + template + void deserialize(Des& des, Steinberg::FUID& uid, Fnc&&) const { + Steinberg::FUID::String uid_str; + des.text1b(uid_str); + + uid.fromString(uid_str); + } +}; + +} // namespace ext + +namespace traits { + +template <> +struct ExtensionTraits { + using TValue = void; + static constexpr bool SupportValueOverload = false; + static constexpr bool SupportObjectOverload = true; + static constexpr bool SupportLambdaOverload = false; +}; + +} // namespace traits +} // namespace bitsery diff --git a/src/common/communication.h b/src/common/communication.h deleted file mode 100644 index 968ac1e6..00000000 --- a/src/common/communication.h +++ /dev/null @@ -1,1084 +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 . - -#pragma once - -#include - -#include -#include - -#ifdef __WINE__ -#include "../wine-host/boost-fix.h" -#endif -#include -#include -#include -#include -#include - -#include "logging.h" - -template -using OutputAdapter = bitsery::OutputBufferAdapter; - -template -using InputAdapter = bitsery::InputBufferAdapter; - -/** - * Serialize an object using bitsery and write it to a socket. This will write - * both the size of the serialized object and the object itself over the socket. - * - * @param socket The Boost.Asio socket to write to. - * @param object The object to write to the stream. - * @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 -inline void write_object(Socket& socket, - const T& object, - std::vector& buffer) { - const size_t size = - bitsery::quickSerialization>>( - buffer, object); - - // Tell the other side how large the object is so it can prepare a buffer - // large enough before sending the data - // NOTE: We're writing these sizes as a 64 bit integers, **not** as pointer - // sized integers. This is to provide compatibility with the 32-bit - // bit bridge. This won't make any function difference aside from the - // 32-bit host application having to convert between 64 and 32 bit - // integers. - boost::asio::write(socket, - boost::asio::buffer(std::array{size})); - const size_t bytes_written = - boost::asio::write(socket, boost::asio::buffer(buffer, size)); - assert(bytes_written == size); -} - -/** - * `write_object()` with a small default buffer for convenience. - * - * @overload - */ -template -inline void write_object(Socket& socket, const T& object) { - std::vector buffer(64); - write_object(socket, object, buffer); -} - -/** - * Deserialize an object by reading it from a socket. This should be used - * together with `write_object`. This will block until the object is available. - * - * @param socket The Boost.Asio socket to read from. - * @param buffer The buffer to read into. This is useful for sending audio and - * chunk data since that can vary in size by a lot. - * - * @return The deserialized object. - * - * @throw std::runtime_error If the conversion to an object was not successful. - * @throw boost::system::system_error If the socket is closed or gets closed - * while reading. - * - * @relates write_object - */ -template -inline T read_object(Socket& socket, std::vector& buffer) { - // See the note above on the use of `uint64_t` instead of `size_t` - std::array message_length; - boost::asio::read(socket, boost::asio::buffer(message_length), - boost::asio::transfer_exactly(sizeof(message_length))); - - // Make sure the buffer is large enough - const size_t size = message_length[0]; - buffer.resize(size); - - // `boost::asio::read/write` will handle all the packet splitting and - // merging for us, since local domain sockets have packet limits somewhere - // in the hundreds of kilobytes - boost::asio::read(socket, boost::asio::buffer(buffer), - boost::asio::transfer_exactly(size)); - - T object; - auto [_, success] = - bitsery::quickDeserialization>>( - {buffer.begin(), size}, object); - - if (BOOST_UNLIKELY(!success)) { - throw std::runtime_error("Deserialization failure in call: " + - std::string(__PRETTY_FUNCTION__)); - } - - return object; -} - -/** - * `read_object()` with a small default buffer for convenience. - * - * @overload - */ -template -inline T read_object(Socket& socket) { - std::vector buffer(64); - return read_object(socket, buffer); -} - -/** - * A single, long-living socket - */ -class SocketHandler { - public: - /** - * Sets up the sockets and start listening on the socket on the listening - * side. The sockets won't be active until `connect()` gets called. - * - * @param io_context The IO context the socket should be bound to. - * @param endpoint The endpoint this socket should connect to or listen on. - * @param listen If `true`, start listening on the sockets. Incoming - * connections will be accepted when `connect()` gets called. This should - * be set to `true` on the plugin side, and `false` on the Wine host side. - * - * @see Sockets::connect - */ - SocketHandler(boost::asio::io_context& io_context, - boost::asio::local::stream_protocol::endpoint endpoint, - bool listen) - : endpoint(endpoint), socket(io_context) { - if (listen) { - boost::filesystem::create_directories( - boost::filesystem::path(endpoint.path()).parent_path()); - acceptor.emplace(io_context, endpoint); - } - } - - /** - * Depending on the value of the `listen` argument passed to the - * constructor, either accept connections made to the sockets on the Linux - * side or connect to the sockets on the Wine side. - */ - void connect() { - if (acceptor) { - acceptor->accept(socket); - } else { - socket.connect(endpoint); - } - } - - /** - * Close the socket. Both sides that are actively listening will be thrown a - * `boost::system_error` when this happens. - */ - void close() { - // The shutdown can fail when the socket is already closed - boost::system::error_code err; - socket.shutdown( - boost::asio::local::stream_protocol::socket::shutdown_both, err); - socket.close(); - } - - /** - * Serialize an object and send it over the socket. - * - * @param object The object to send. - * @param buffer The buffer to use for the serialization. This is used to - * prevent excess allocations when sending audio. - * - * @throw boost::system::system_error If the socket is closed or gets closed - * during sending. - * - * @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. - * - * @see write_object - * @see SocketHandler::receive_single - * @see SocketHandler::receive_multi - */ - template - inline void send(const T& object, std::vector& buffer) { - write_object(socket, object, buffer); - } - - /** - * `SocketHandler::send()` with a small default buffer for convenience. - * - * @overload - */ - template - inline void send(const T& object) { - write_object(socket, object); - } - - /** - * Read a serialized object from the socket sent using `send()`. This will - * block until the object is available. - * - * @param buffer The buffer to read into. This is used to prevent excess - * allocations when sending audio. - * - * @return The deserialized object. - * - * @throw std::runtime_error If the conversion to an object was not - * successful. - * @throw boost::system::system_error If the socket is closed or gets closed - * while reading. - * - * @relates SocketHandler::send - * - * @see read_object - * @see SocketHandler::receive_multi - */ - template - inline T receive_single(std::vector& buffer) { - return read_object(socket, buffer); - } - - /** - * `SocketHandler::receive_single()` with a small default buffer for - * convenience. - * - * @overload - */ - template - inline T receive_single() { - return read_object(socket); - } - - /** - * Start a blocking loop to receive objects on this socket. This function - * will return once the socket gets closed. - * - * @param callback A function that gets passed the received object. Since - * we'd probably want to do some more stuff after sending a reply, calling - * `send()` is the responsibility of this function. - * - * @tparam F A function type in the form of `void(T, std::vector&)` - * that does something with the object, and then calls `send()`. The - * reading/writing buffer is passed along so it can be reused for sending - * large amounts of data. - * - * @relates SocketHandler::send - * - * @see read_object - * @see SocketHandler::receive_single - */ - template - void receive_multi(F callback) { - std::vector buffer{}; - while (true) { - try { - auto object = receive_single(buffer); - - callback(std::move(object), buffer); - } catch (const boost::system::system_error&) { - // This happens when the sockets got closed because the plugin - // is being shut down - break; - } - } - } - - private: - boost::asio::local::stream_protocol::endpoint endpoint; - boost::asio::local::stream_protocol::socket socket; - - /** - * Will be used in `connect()` on the listening side to establish the - * connection. - */ - std::optional acceptor; -}; - -/** - * Encodes the base behavior for reading from and writing to the `data` argument - * for event dispatch functions. This provides base functionality for these - * kinds of events. The `dispatch()` function will require some more specific - * structs. - */ -class DefaultDataConverter { - public: - virtual ~DefaultDataConverter(){}; - - /** - * Read data from the `data` void pointer into a an `EventPayload` value - * that can be serialized and conveys the meaning of the event. - */ - virtual EventPayload read(const int opcode, - const int index, - const intptr_t value, - const void* data) const; - - /** - * Read data from the `value` pointer into a an `EventPayload` value that - * can be serialized and conveys the meaning of the event. This is only used - * for the `effSetSpeakerArrangement` and `effGetSpeakerArrangement` events. - */ - virtual std::optional read_value(const int opcode, - const intptr_t value) const; - - /** - * Write the reponse back to the `data` pointer. - */ - virtual void write(const int opcode, - void* data, - const EventResult& response) const; - - /** - * Write the reponse back to the `value` pointer. This is only used during - * the `effGetSpeakerArrangement` event. - */ - virtual void write_value(const int opcode, - intptr_t value, - const EventResult& response) const; - - /** - * This function can override a callback's return value based on the opcode. - * This is used in one place to return a pointer to a `VstTime` object - * that's contantly being updated. - * - * @param opcode The opcode for the current event. - * @param original The original return value as returned by the callback - * function. - */ - virtual intptr_t return_value(const int opcode, - const intptr_t original) const; -}; - -/** - * So, this is a bit of a mess. The TL;DR is that we want to use a single long - * living socket connection for `dispatch()` and another one for `audioMaster()` - * for performance reasons, but when the socket is already being written to we - * create new connections on demand. - * - * For most of our sockets we can just send out our messages on the writing - * side, and do a simple blocking loop on the reading side. The `dispatch()` and - * `audioMaster()` calls are different. Not only do they have they come with - * complex payload values, they can also be called simultaneously from multiple - * threads, and `audioMaster()` and `dispatch()` calls can even be mutually - * recursive. Luckily this does not happen very often, but it does mean that our - * simple 'one-socket-per-function' model doesn't work anymore. Because setting - * up new sockets is quite expensive and this is seldom needed, this works - * slightly differently: - * - * - We'll keep a single long lived socket connection. This works the exact same - * way as every other socket defined in the `Sockets` class. - * - Aside from that the listening side will have a second thread asynchronously - * listening for new connections on the socket endpoint. - * - * The `EventHandler::send_event()` method is used to send events. If the socket - * is currently being written to, we'll first create a new socket connection as - * described above. Similarly, the `EventHandler::receive_events()` method first - * sets up asynchronous listeners for the socket endpoint, and then block and - * handle events until the main socket is closed. - * - * @tparam Thread The thread implementation to use. On the Linux side this - * should be `std::jthread` and on the Wine side this should be `Win32Thread`. - */ -template -class EventHandler { - public: - /** - * Sets up a single main socket for this type of events. The sockets won't - * be active until `connect()` gets called. - * - * @param io_context The IO context the main socket should be bound to. A - * new IO context will be created for accepting the additional incoming - * connections. - * @param endpoint The socket endpoint used for this event handler. - * @param listen If `true`, start listening on the sockets. Incoming - * connections will be accepted when `connect()` gets called. This should - * be set to `true` on the plugin side, and `false` on the Wine host side. - * - * @see Sockets::connect - */ - EventHandler(boost::asio::io_context& io_context, - boost::asio::local::stream_protocol::endpoint endpoint, - bool listen) - : io_context(io_context), endpoint(endpoint), socket(io_context) { - if (listen) { - boost::filesystem::create_directories( - boost::filesystem::path(endpoint.path()).parent_path()); - acceptor.emplace(io_context, endpoint); - } - } - - /** - * Depending on the value of the `listen` argument passed to the - * constructor, either accept connections made to the sockets on the Linux - * side or connect to the sockets on the Wine side - */ - void connect() { - if (acceptor) { - acceptor->accept(socket); - - // As mentioned in `acceptor's` docstring, this acceptor will be - // recreated in `receive_events()` on another context, and - // potentially on the other side of the connection in the case of - // `vst_host_callback` - acceptor.reset(); - boost::filesystem::remove(endpoint.path()); - } else { - socket.connect(endpoint); - } - } - - /** - * Close the socket. Both sides that are actively listening will be thrown a - * `boost::system_error` when this happens. - */ - void close() { - // The shutdown can fail when the socket is already closed - boost::system::error_code err; - socket.shutdown( - boost::asio::local::stream_protocol::socket::shutdown_both, err); - socket.close(); - } - - /** - * Serialize and send an event over a socket. This is used for both the host - * -> plugin 'dispatch' events and the plugin -> host 'audioMaster' host - * callbacks since they follow the same format. See one of those functions - * for details on the parameters and return value of this function. - * - * As described above, if this function is currently being called from - * another thread, then this will create a new socket connection and send - * the event there instead. - * - * @param data_converter Some struct that knows how to read data from and - * write data back to the `data` void pointer. For host callbacks this - * parameter contains either a string or a null pointer while `dispatch()` - * calls might contain opcode specific structs. See the documentation for - * `EventPayload` for more information. The `DefaultDataConverter` defined - * above handles the basic behavior that's sufficient for host callbacks. - * @param logging A pair containing a logger instance and whether or not - * this is for sending `dispatch()` events or host callbacks. Optional - * since it doesn't have to be done on both sides. - * - * @relates EventHandler::receive_events - * @relates passthrough_event - */ - template - intptr_t send_event(D& data_converter, - std::optional> logging, - int opcode, - int index, - intptr_t value, - void* data, - float option) { - // Encode the right payload types for this event. Check the - // documentation for `EventPayload` for more information. These types - // are converted to C-style data structures in `passthrough_event()` so - // they can be passed to a plugin or callback function. - const EventPayload payload = - data_converter.read(opcode, index, value, data); - const std::optional value_payload = - data_converter.read_value(opcode, value); - - if (logging) { - auto [logger, is_dispatch] = *logging; - logger.log_event(is_dispatch, opcode, index, value, payload, option, - value_payload); - } - - const Event event{.opcode = opcode, - .index = index, - .value = value, - .option = option, - .payload = payload, - .value_payload = value_payload}; - - // A socket only handles a single request at a time as to prevent - // messages from arriving out of order. For throughput reasons we prefer - // to do most communication over a single main socket (`socket`), and - // we'll lock `write_mutex` while doing so. In the event that the mutex - // is already locked and thus the main socket is currently in use by - // another thread, then we'll spawn a new socket to handle the request. - EventResult response; - { - std::unique_lock lock(write_mutex, std::try_to_lock); - if (lock.owns_lock()) { - write_object(socket, event); - response = read_object(socket); - } else { - try { - boost::asio::local::stream_protocol::socket - secondary_socket(io_context); - secondary_socket.connect(endpoint); - - write_object(secondary_socket, event); - response = read_object(secondary_socket); - } catch (const boost::system::system_error& e) { - // So, what do we do when noone is listening on the endpoint - // yet? This can happen with plugin groups when the Wine - // host process does an `audioMaster()` call before the - // plugin is listening. If that happens we'll fall back to a - // synchronous request. This is not very pretty, so if - // anyone can think of a better way to structure all of this - // while still mainting a long living primary socket please - // let me know. - // Note that this should **only** be done before the call to - // `connect()`. If we get here at any other point then it - // means that the plugin side is no longer listening on the - // sockets, and we should thus just exit. - if (!sent_first_event) { - std::lock_guard lock(write_mutex); - - write_object(socket, event); - response = read_object(socket); - } else { - // Rethrow the exception if the sockets we're not - // handling the specific case described above - throw e; - } - } - } - } - - // This was used to always block when sending the first message, because - // the other side may not be listening for additional connections yet - sent_first_event = true; - - if (logging) { - auto [logger, is_dispatch] = *logging; - logger.log_event_response(is_dispatch, opcode, - response.return_value, response.payload, - response.value_payload); - } - - data_converter.write(opcode, data, response); - data_converter.write_value(opcode, value, response); - - return data_converter.return_value(opcode, response.return_value); - } - - /** - * Spawn a new thread to listen for extra connections to `endpoint`, and - * then a blocking loop that handles events from the primary `socket`. - * - * The specified function will be used to create an `EventResult` from an - * `Event`. This is almost uses `passthrough_event()`, which converts a - * `EventPayload` into the format used by VST2, calls either `dispatch()` or - * `audioMaster()` depending on the context, and then serializes the result - * back into an `EventResultPayload`. - * - * This function will also be used separately for receiving MIDI data, as - * some plugins will need pointers to received MIDI data to stay alive until - * the next audio buffer gets processed. - * - * @param logging A pair containing a logger instance and whether or not - * this is for sending `dispatch()` events or host callbacks. Optional - * since it doesn't have to be done on both sides. - * @param callback The function used to generate a response out of an event. - * See the definition of `F` for more information. - * - * @tparam F A function type in the form of `EventResponse(Event, bool)`. - * The boolean flag is `true` when this event was received on the main - * socket, and `false` otherwise. - * - * @relates EventHandler::send_event - * @relates passthrough_event - */ - template - void receive_events(std::optional> logging, - F callback) { - // Reading, processing, and writing back event data from the sockets - // works in the same way regardless of which socket we're using - const auto process_event = - [&](boost::asio::local::stream_protocol::socket& socket, - bool on_main_thread) { - auto event = read_object(socket); - if (logging) { - auto [logger, is_dispatch] = *logging; - logger.log_event(is_dispatch, event.opcode, event.index, - event.value, event.payload, event.option, - event.value_payload); - } - - EventResult response = callback(event, on_main_thread); - if (logging) { - auto [logger, is_dispatch] = *logging; - logger.log_event_response( - is_dispatch, event.opcode, response.return_value, - response.payload, response.value_payload); - } - - write_object(socket, response); - }; - - // As described above we'll handle incoming requests for `socket` on - // this thread. We'll also listen for incoming connections on `endpoint` - // on another thread. For any incoming connection we'll spawn a new - // thread to handle the request. When `socket` closes and this loop - // breaks, the listener and any still active threads will be cleaned up - // before this function exits. - boost::asio::io_context secondary_context{}; - - // The previous acceptor has already been shut down by - // `EventHandler::connect()` - acceptor.emplace(secondary_context, endpoint); - - // This works the exact same was as `active_plugins` and - // `next_plugin_id` in `GroupBridge` - std::map active_secondary_requests{}; - std::atomic_size_t next_request_id{}; - std::mutex active_secondary_requests_mutex{}; - accept_requests( - *acceptor, logging, - [&](boost::asio::local::stream_protocol::socket secondary_socket) { - const size_t request_id = next_request_id.fetch_add(1); - - // We have to make sure to keep moving these sockets into the - // threads that will handle them - std::lock_guard lock(active_secondary_requests_mutex); - active_secondary_requests[request_id] = Thread( - [&, request_id](boost::asio::local::stream_protocol::socket - secondary_socket) { - process_event(secondary_socket, false); - - // When we have processed this request, we'll join the - // thread again with the thread that's handling - // `secondary_context`. - boost::asio::post(secondary_context, [&, request_id]() { - std::lock_guard lock( - active_secondary_requests_mutex); - - // The join is implicit because we're using - // std::jthread/Win32Thread - active_secondary_requests.erase(request_id); - }); - }, - std::move(secondary_socket)); - }); - - Thread secondary_requests_handler([&]() { secondary_context.run(); }); - - while (true) { - try { - process_event(socket, true); - } catch (const boost::system::system_error&) { - // This happens when the sockets got closed because the plugin - // is being shut down - break; - } - } - - // After the main socket gets terminated (during shutdown) we'll make - // sure all outstanding jobs have been processed and then drop all work - // from the IO context - std::lock_guard lock(active_secondary_requests_mutex); - secondary_context.stop(); - acceptor.reset(); - } - - private: - /** - * Used in `receive_events()` to asynchronously listen for secondary socket - * connections. After `callback()` returns this function will continue to be - * called until the IO context gets stopped. - * - * @param acceptor The acceptor we will be listening on. - * @param logging A pair containing a logger instance and whether or not - * this is for sending `dispatch()` events or host callbacks. Optional - * since it doesn't have to be done on both sides. - * @param callback A function that handles the new socket connection. - * - * @tparam F A function in the form - * `void(boost::asio::local::stream_protocol::socket)` to handle a new - * incoming connection. - */ - template - void accept_requests( - boost::asio::local::stream_protocol::acceptor& acceptor, - std::optional> logging, - F callback) { - acceptor.async_accept( - [&, logging, callback]( - const boost::system::error_code& error, - boost::asio::local::stream_protocol::socket secondary_socket) { - if (error.failed()) { - // On the Wine side it's expected that the main socket - // connection will be dropped during shutdown, so we can - // silently ignore any related socket errors on the Wine - // side - if (logging) { - auto [logger, is_dispatch] = *logging; - logger.log("Failure while accepting connections: " + - error.message()); - } - - return; - } - - callback(std::move(secondary_socket)); - - accept_requests(acceptor, logging, callback); - }); - } - - /** - * The main IO context. New sockets created during `send_event()` will be - * bound to this context. In `receive_events()` we'll create a new IO - * context since we want to do all listening there on a dedicated thread. - */ - boost::asio::io_context& io_context; - - boost::asio::local::stream_protocol::endpoint endpoint; - boost::asio::local::stream_protocol::socket socket; - - /** - * This acceptor will be used once synchronously on the listening side - * during `Sockets::connect()`. When `EventHandler::receive_events()` is - * then called, we'll recreate the acceptor to asynchronously listen for new - * incoming socket connections on `endpoint` using. This is important, - * because on the case of `vst_host_callback` the acceptor is first accepts - * an initial socket on the plugin side (like all sockets), but all - * additional incoming connections of course have to be listened for on the - * plugin side. - */ - std::optional acceptor; - - /** - * A mutex that locks the main `socket`. If this is locked, then any new - * events will be sent over a new socket instead. - */ - std::mutex write_mutex; - - private: - /** - * Indicates whether or not the remove has processed an event we sent from - * this side. When a Windows VST2 plugin performs a host callback in its - * constructor, before the native plugin has had time to connect to the - * sockets, we want it to always wait for the sockets to come online, but - * this fallback behaviour should only happen during initialization. - */ - std::atomic_bool sent_first_event = false; -}; - -/** - * Manages all the sockets used for communicating between the plugin and the - * Wine host. Every plugin will get its own directory (the socket endpoint base - * directory), and all socket endpoints are created within this directory. This - * is usually `/run/user//yabridge--/`. - * - * On the plugin side this class should be initialized with `listen` set to - * `true` before launching the Wine VST host. This will start listening on the - * sockets, and the call to `connect()` will then accept any incoming - * connections. - * - * @tparam Thread The thread implementation to use. On the Linux side this - * should be `std::jthread` and on the Wine side this should be `Win32Thread`. - */ -template -class Sockets { - public: - /** - * Sets up the sockets using the specified base directory. The sockets won't - * be active until `connect()` gets called. - * - * @param io_context The IO context the sockets should be bound to. Relevant - * when doing asynchronous operations. - * @param endpoint_base_dir The base directory that will be used for the - * Unix domain sockets. - * @param listen If `true`, start listening on the sockets. Incoming - * connections will be accepted when `connect()` gets called. This should - * be set to `true` on the plugin side, and `false` on the Wine host side. - * - * @see Sockets::connect - */ - Sockets(boost::asio::io_context& io_context, - const boost::filesystem::path& endpoint_base_dir, - bool listen) - : base_dir(endpoint_base_dir), - host_vst_dispatch(io_context, - (base_dir / "host_vst_dispatch.sock").string(), - listen), - vst_host_callback(io_context, - (base_dir / "vst_host_callback.sock").string(), - listen), - host_vst_parameters(io_context, - (base_dir / "host_vst_parameters.sock").string(), - listen), - host_vst_process_replacing( - io_context, - (base_dir / "host_vst_process_replacing.sock").string(), - listen), - host_vst_control(io_context, - (base_dir / "host_vst_control.sock").string(), - listen) {} - - /** - * Cleans up the directory containing the socket endpoints when yabridge - * shuts down if it still exists. - */ - ~Sockets() { - // Manually close all sockets so we break out of any blocking operations - // that may still be active - host_vst_dispatch.close(); - vst_host_callback.close(); - host_vst_parameters.close(); - host_vst_process_replacing.close(); - host_vst_control.close(); - - // Only clean if we're the ones who have created these files, although - // it should not cause any harm to also do this on the Wine side - try { - boost::filesystem::remove_all(base_dir); - } catch (const boost::filesystem::filesystem_error&) { - // There should not be any filesystem errors since only one side - // removes the files, but if we somehow can't delete the file - // then we can just silently ignore this - } - } - - /** - * Depending on the value of the `listen` argument passed to the - * constructor, either accept connections made to the sockets on the Linux - * side or connect to the sockets on the Wine side - */ - void connect() { - host_vst_dispatch.connect(); - vst_host_callback.connect(); - host_vst_parameters.connect(); - host_vst_process_replacing.connect(); - host_vst_control.connect(); - } - - /** - * The base directory for our socket endpoints. All `*_endpoint` variables - * below are files within this directory. - */ - const boost::filesystem::path base_dir; - - // The naming convention for these sockets is `__`. For - // instance the socket named `host_vst_dispatch` forwards - // `AEffect.dispatch()` calls from the native VST host to the Windows VST - // plugin (through the Wine VST host). - - /** - * The socket that forwards all `dispatcher()` calls from the VST host to - * the plugin. - */ - EventHandler host_vst_dispatch; - /** - * The socket that forwards all `audioMaster()` calls from the Windows VST - * plugin to the host. - */ - EventHandler vst_host_callback; - /** - * Used for both `getParameter` and `setParameter` since they mostly - * overlap. - */ - SocketHandler host_vst_parameters; - /** - * Used for processing audio usign the `process()`, `processReplacing()` and - * `processDoubleReplacing()` functions. - */ - SocketHandler host_vst_process_replacing; - /** - * A control socket that sends data that is not suitable for the other - * sockets. At the moment this is only used to, on startup, send the Windows - * VST plugin's `AEffect` object to the native VST plugin, and to then send - * the configuration (from `config`) back to the Wine host. - */ - SocketHandler host_vst_control; -}; - -/** - * Generate a unique base directory that can be used as a prefix for all Unix - * domain socket endpoints used in `PluginBridge`/`Vst2Bridge`. This will - * usually return `/run/user//yabridge--/`. - * - * Sockets for group hosts are handled separately. See - * `../plugin/utils.h:generate_group_endpoint` for more information on those. - * - * @param plugin_name The name of the plugin we're generating endpoints for. - * Used as a visual indication of what plugin is using this endpoint. - */ -boost::filesystem::path generate_endpoint_base(const std::string& plugin_name); - -/** - * Unmarshall an `EventPayload` back to the representation used by VST2, pass - * that value to a callback function (either `AEffect::dispatcher()` for host -> - * plugin events or `audioMaster()` for plugin -> host events), and then - * serialize the results back into an `EventResult`. - * - * This is the receiving analogue of the `*DataCovnerter` objects. - * - * @param plugin The `AEffect` instance that should be passed to the callback - * function. During `WantsAEffect` we'll send back a copy of this, and when we - * get sent an `AEffect` instance (e.g. during `audioMasterIOChanged()`) we'll - * write the updated values to this isntance. - * @param callback The function to call with the arguments received from the - * socket, either `AEffect::dispatcher()` or `audioMasterCallback()`. - * - * @tparam F A function with the same signature as `AEffect::dispatcher` or - * `audioMasterCallback`. - * - * @return The result of the operation. If necessary the `DataConverter` will - * unmarshall the payload again and write it back. - * - * @relates EventHandler::receive_events - */ -template -EventResult passthrough_event(AEffect* plugin, F callback, Event& event) { - // This buffer is used to write strings and small objects to. We'll - // initialize the beginning with null values to both prevent it from being - // read as some arbitrary C-style string, and to make sure that - // `*static_cast(string_buffer.data)` will be a null pointer if the - // plugin is supposed to write a pointer there but doesn't (such as with - // `effEditGetRect`/`WantsVstRect`). - std::array string_buffer; - std::fill(string_buffer.begin(), string_buffer.begin() + sizeof(size_t), 0); - - auto read_payload_fn = overload{ - [&](const std::nullptr_t&) -> void* { return nullptr; }, - [&](const std::string& s) -> void* { - return const_cast(s.c_str()); - }, - [&](const ChunkData& chunk) -> void* { - return const_cast(chunk.buffer.data()); - }, - [&](native_size_t& window_handle) -> void* { - // This is the X11 window handle that the editor should reparent - // itself to. We have a special wrapper around the dispatch function - // that intercepts `effEditOpen` events and creates a Win32 window - // and then finally embeds the X11 window Wine created into this - // wnidow handle. Make sure to convert the window ID first to - // `size_t` in case this is the 32-bit host. - return reinterpret_cast(static_cast(window_handle)); - }, - [&](const AEffect&) -> void* { return nullptr; }, - [&](DynamicVstEvents& events) -> void* { - return &events.as_c_events(); - }, - [&](DynamicSpeakerArrangement& speaker_arrangement) -> void* { - return &speaker_arrangement.as_c_speaker_arrangement(); - }, - [&](WantsAEffectUpdate&) -> void* { - // The host will never actually ask for an updated `AEffect` object - // since that should not be a thing. This is purely a meant as a - // workaround for plugins that initialize their `AEffect` object - // after the plugin has already finished initializing. - return nullptr; - }, - [&](WantsChunkBuffer&) -> void* { return string_buffer.data(); }, - [&](VstIOProperties& props) -> void* { return &props; }, - [&](VstMidiKeyName& key_name) -> void* { return &key_name; }, - [&](VstParameterProperties& props) -> void* { return &props; }, - [&](WantsVstRect&) -> void* { return string_buffer.data(); }, - [&](const WantsVstTimeInfo&) -> void* { return nullptr; }, - [&](WantsString&) -> void* { return string_buffer.data(); }}; - - // Almost all events pass data through the `data` argument. There are two - // events, `effSetParameter` and `effGetParameter` that also pass data - // through the value argument. - void* data = std::visit(read_payload_fn, event.payload); - intptr_t value = event.value; - if (event.value_payload) { - value = reinterpret_cast( - std::visit(read_payload_fn, *event.value_payload)); - } - - const intptr_t return_value = - callback(plugin, event.opcode, event.index, value, data, event.option); - - // Only write back data when needed, this depends on the event payload type - auto write_payload_fn = overload{ - [&](auto) -> EventResultPayload { return nullptr; }, - [&](const AEffect& updated_plugin) -> EventResultPayload { - // This is a bit of a special case! Instead of writing some return - // value, we will update values on the native VST plugin's `AEffect` - // object. This is triggered by the `audioMasterIOChanged` callback - // from the hosted VST plugin. - update_aeffect(*plugin, updated_plugin); - - return nullptr; - }, - [&](DynamicSpeakerArrangement& speaker_arrangement) - -> EventResultPayload { return speaker_arrangement; }, - [&](WantsAEffectUpdate&) -> EventResultPayload { return *plugin; }, - [&](WantsChunkBuffer&) -> EventResultPayload { - // In this case the plugin will have written its data stored in an - // array to which a pointer is stored in `data`, with the return - // value from the event determines how much data the plugin has - // written - const uint8_t* chunk_data = *static_cast(data); - return ChunkData{ - std::vector(chunk_data, chunk_data + return_value)}; - }, - [&](WantsVstRect&) -> EventResultPayload { - // The plugin should have written a pointer to a VstRect struct into - // the data pointer. I haven't seen this fail yet, but since some - // hosts will call `effEditGetRect()` before `effEditOpen()` I can - // assume there are plugins that don't handle this correctly. - VstRect* editor_rect = *static_cast(data); - if (!editor_rect) { - return nullptr; - } - - return *editor_rect; - }, - [&](WantsVstTimeInfo&) -> EventResultPayload { - // Not sure why the VST API has twenty different ways of - // returning structs, but in this case the value returned from - // the callback function is actually a pointer to a - // `VstTimeInfo` struct! It can also be a null pointer if the - // host doesn't support this. - const auto time_info = - reinterpret_cast(return_value); - if (!time_info) { - return nullptr; - } else { - return *time_info; - } - }, - [&](WantsString&) -> EventResultPayload { - return std::string(static_cast(data)); - }, - [&](VstIOProperties& props) -> EventResultPayload { return props; }, - [&](VstMidiKeyName& key_name) -> EventResultPayload { - return key_name; - }, - [&](VstParameterProperties& props) -> EventResultPayload { - return props; - }}; - - // As mentioned about, the `effSetSpeakerArrangement` and - // `effGetSpeakerArrangement` events are the only two events that use the - // value argument as a pointer to write data to. Additionally, the - // `effGetSpeakerArrangement` expects the plugin to write its own data to - // this value. Hence why we need to encode the response here separately. - const EventResultPayload response_data = - std::visit(write_payload_fn, event.payload); - std::optional value_response_data = std::nullopt; - if (event.value_payload) { - value_response_data = - std::visit(write_payload_fn, *event.value_payload); - } - - EventResult response{.return_value = return_value, - .payload = response_data, - .value_payload = value_response_data}; - - return response; -} diff --git a/src/common/communication/common.cpp b/src/common/communication/common.cpp new file mode 100644 index 00000000..a4fba588 --- /dev/null +++ b/src/common/communication/common.cpp @@ -0,0 +1,38 @@ +#include "common.h" + +#include + +#include "../utils.h" + +namespace fs = boost::filesystem; + +/** + * Used for generating random identifiers. + */ +constexpr char alphanumeric_characters[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +boost::filesystem::path generate_endpoint_base(const std::string& plugin_name) { + fs::path temp_directory = get_temporary_directory(); + + std::random_device random_device; + std::mt19937 rng(random_device()); + fs::path candidate_endpoint; + do { + std::string random_id; + std::sample( + alphanumeric_characters, + alphanumeric_characters + strlen(alphanumeric_characters) - 1, + std::back_inserter(random_id), 8, rng); + + // We'll get rid of the file descriptors immediately after accepting the + // sockets, so putting them inside of a subdirectory would only leave + // behind an empty directory + std::ostringstream socket_name; + socket_name << "yabridge-" << plugin_name << "-" << random_id; + + candidate_endpoint = temp_directory / socket_name.str(); + } while (fs::exists(candidate_endpoint)); + + return candidate_endpoint; +} diff --git a/src/common/communication/common.h b/src/common/communication/common.h new file mode 100644 index 00000000..ef9a34be --- /dev/null +++ b/src/common/communication/common.h @@ -0,0 +1,769 @@ +// 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 . + +#pragma once + +#include + +#include +#include +#include + +#ifdef __WINE__ +#include "../wine-host/boost-fix.h" +#endif +#include +#include +#include +#include +#include + +#include "../logging/common.h" + +template +using OutputAdapter = bitsery::OutputBufferAdapter; + +template +using InputAdapter = bitsery::InputBufferAdapter; + +/** + * Serialize an object using bitsery and write it to a socket. This will write + * both the size of the serialized object and the object itself over the socket. + * + * @param socket The Boost.Asio socket to write to. + * @param object The object to write to the stream. + * @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 +inline void write_object(Socket& socket, + const T& object, + std::vector& buffer) { + const size_t size = + bitsery::quickSerialization>>( + buffer, object); + + // Tell the other side how large the object is so it can prepare a buffer + // large enough before sending the data + // NOTE: We're writing these sizes as a 64 bit integers, **not** as pointer + // sized integers. This is to provide compatibility with the 32-bit + // bit bridge. This won't make any function difference aside from the + // 32-bit host application having to convert between 64 and 32 bit + // integers. + boost::asio::write(socket, + boost::asio::buffer(std::array{size})); + const size_t bytes_written = + boost::asio::write(socket, boost::asio::buffer(buffer, size)); + assert(bytes_written == size); +} + +/** + * `write_object()` with a small default buffer for convenience. + * + * @overload + */ +template +inline void write_object(Socket& socket, const T& object) { + std::vector buffer(64); + write_object(socket, object, buffer); +} + +/** + * Deserialize an object by reading it from a socket. This should be used + * together with `write_object`. This will block until the object is available. + * + * @param socket The Boost.Asio socket to read from. + * @param buffer The buffer to read into. This is useful for sending audio and + * chunk data since that can vary in size by a lot. + * @param object The object to serialize into. There are also overrides that + * create a new default initialized `T` + * + * @return The deserialized object. + * + * @throw std::runtime_error If the conversion to an object was not successful. + * @throw boost::system::system_error If the socket is closed or gets closed + * while reading. + * + * TODO: Swap these arguments around so they match `write_object`'s argument + * order + * + * @relates write_object + */ +template +inline T& read_object(Socket& socket, std::vector& buffer, T& object) { + // See the note above on the use of `uint64_t` instead of `size_t` + std::array message_length; + boost::asio::read(socket, boost::asio::buffer(message_length), + boost::asio::transfer_exactly(sizeof(message_length))); + + // Make sure the buffer is large enough + const size_t size = message_length[0]; + buffer.resize(size); + + // `boost::asio::read/write` will handle all the packet splitting and + // merging for us, since local domain sockets have packet limits somewhere + // in the hundreds of kilobytes + boost::asio::read(socket, boost::asio::buffer(buffer), + boost::asio::transfer_exactly(size)); + + auto [_, success] = + bitsery::quickDeserialization>>( + {buffer.begin(), size}, object); + + if (BOOST_UNLIKELY(!success)) { + throw std::runtime_error("Deserialization failure in call: " + + std::string(__PRETTY_FUNCTION__)); + } + + return object; +} + +/** + * `read_object()` into a new default initialized object with an existing + * buffer. + * + * @overload + */ +template +inline T read_object(Socket& socket, std::vector& buffer) { + T object; + read_object(socket, buffer, object); + + return object; +} + +/** + * `read_object()` into an existing object a small default + * buffer for convenience. + * + * @overload + */ +template +inline T& read_object(Socket& socket, T& object) { + std::vector buffer(64); + return read_object(socket, buffer, object); +} + +/** + * `read_object()` into a new default initialized object with a small default + * buffer for convenience. + * + * @overload + */ +template +inline T read_object(Socket& socket) { + T object; + std::vector buffer(64); + read_object(socket, buffer, object); + + return object; +} + +/** + * Generate a unique base directory that can be used as a prefix for all Unix + * domain socket endpoints used in `Vst2PluginBridge`/`Vst2Bridge`. This will + * usually return `/run/user//yabridge--/`. + * + * Sockets for group hosts are handled separately. See + * `../plugin/utils.h:generate_group_endpoint` for more information on those. + * + * @param plugin_name The name of the plugin we're generating endpoints for. + * Used as a visual indication of what plugin is using this endpoint. + */ +boost::filesystem::path generate_endpoint_base(const std::string& plugin_name); + +/** + * Manages all the sockets used for communicating between the plugin and the + * Wine host. Every plugin will get its own directory (the socket endpoint base + * directory), and all socket endpoints are created within this directory. This + * is usually `/run/user//yabridge--/`. + */ +class Sockets { + public: + /** + * Sets up the the base directory for the sockets. Classes inheriting this + * should set up their sockets here. + * + * @param endpoint_base_dir The base directory that will be used for the + * Unix domain sockets. + * + * @see Sockets::connect + */ + Sockets(const boost::filesystem::path& endpoint_base_dir) + : base_dir(endpoint_base_dir) {} + + /** + * Shuts down and closes all sockets and then cleans up the directory + * containing the socket endpoints when yabridge shuts down if it still + * exists. + * + * @note Classes overriding this should call `close()` in their destructor. + */ + virtual ~Sockets() { + try { + boost::filesystem::remove_all(base_dir); + } catch (const boost::filesystem::filesystem_error&) { + // There should not be any filesystem errors since only one side + // removes the files, but if we somehow can't delete the file + // then we can just silently ignore this + } + } + + /** + * Depending on the value of the `listen` argument passed to the + * constructor, either accept connections made to the sockets on the Linux + * side or connect to the sockets on the Wine side. + * + * @remark On the plugin side `PluginBridge::connect_sockets_guarded()` + * should be used instead so we can terminate everything in the event that + * Wine fails to start. + */ + virtual void connect() = 0; + + /** + * Shut down and close all sockets. Called during the destructor and also + * explicitly called when shutting down a plugin in a group host process. + * + * It should be safe to call this function more than once, and it should be + * called in the overridden class's destructor. + */ + virtual void close() = 0; + + /** + * The base directory for our socket endpoints. All `*_endpoint` variables + * below are files within this directory. + */ + const boost::filesystem::path base_dir; +}; + +/** + * A single, long-living socket + */ +class SocketHandler { + public: + /** + * Sets up the sockets and start listening on the socket on the listening + * side. The sockets won't be active until `connect()` gets called. + * + * @param io_context The IO context the socket should be bound to. + * @param endpoint The endpoint this socket should connect to or listen on. + * @param listen If `true`, start listening on the sockets. Incoming + * connections will be accepted when `connect()` gets called. This should + * be set to `true` on the plugin side, and `false` on the Wine host side. + * + * @see Sockets::connect + */ + SocketHandler(boost::asio::io_context& io_context, + boost::asio::local::stream_protocol::endpoint endpoint, + bool listen) + : endpoint(endpoint), socket(io_context) { + if (listen) { + boost::filesystem::create_directories( + boost::filesystem::path(endpoint.path()).parent_path()); + acceptor.emplace(io_context, endpoint); + } + } + + /** + * Depending on the value of the `listen` argument passed to the + * constructor, either accept connections made to the sockets on the Linux + * side or connect to the sockets on the Wine side. + */ + void connect() { + if (acceptor) { + acceptor->accept(socket); + } else { + socket.connect(endpoint); + } + } + + /** + * Close the socket. Both sides that are actively listening will be thrown a + * `boost::system_error` when this happens. + */ + void close() { + // The shutdown can fail when the socket is already closed + boost::system::error_code err; + socket.shutdown( + boost::asio::local::stream_protocol::socket::shutdown_both, err); + socket.close(); + } + + /** + * Serialize an object and send it over the socket. + * + * @param object The object to send. + * @param buffer The buffer to use for the serialization. This is used to + * prevent excess allocations when sending audio. + * + * @throw boost::system::system_error If the socket is closed or gets closed + * during sending. + * + * @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. The caller is responsible for preventing + * this. + * + * @see write_object + * @see SocketHandler::receive_single + * @see SocketHandler::receive_multi + */ + template + inline void send(const T& object, std::vector& buffer) { + write_object(socket, object, buffer); + } + + /** + * `SocketHandler::send()` with a small default buffer for convenience. + * + * @overload + */ + template + inline void send(const T& object) { + write_object(socket, object); + } + + /** + * Read a serialized object from the socket sent using `send()`. This will + * block until the object is available. + * + * @param buffer The buffer to read into. This is used to prevent excess + * allocations when sending audio. + * + * @return The deserialized object. + * + * @throw std::runtime_error If the conversion to an object was not + * successful. + * @throw boost::system::system_error If the socket is closed or gets closed + * while reading. + * + * @note This function can safely be called within the lambda of + * `SocketHandler::receive_multi()`. + * + * @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. The caller is responsible for preventing + * this. + * + * @relates SocketHandler::send + * + * @see read_object + * @see SocketHandler::receive_multi + */ + template + inline T receive_single(std::vector& buffer) { + return read_object(socket, buffer); + } + + /** + * `SocketHandler::receive_single()` with a small default buffer for + * convenience. + * + * @overload + */ + template + inline T receive_single() { + return read_object(socket); + } + + /** + * Start a blocking loop to receive objects on this socket. This function + * will return once the socket gets closed. + * + * @param callback A function that gets passed the received object. Since + * we'd probably want to do some more stuff after sending a reply, calling + * `send()` is the responsibility of this function. + * + * @tparam F A function type in the form of `void(T, std::vector&)` + * that does something with the object, and then calls `send()`. The + * reading/writing buffer is passed along so it can be reused for sending + * large amounts of data. + * + * @relates SocketHandler::send + * + * @see read_object + * @see SocketHandler::receive_single + */ + template + void receive_multi(F callback) { + std::vector buffer{}; + while (true) { + try { + auto object = receive_single(buffer); + + callback(std::move(object), buffer); + } catch (const boost::system::system_error&) { + // This happens when the sockets got closed because the plugin + // is being shut down + break; + } + } + } + + private: + boost::asio::local::stream_protocol::endpoint endpoint; + boost::asio::local::stream_protocol::socket socket; + + /** + * Will be used in `connect()` on the listening side to establish the + * connection. + */ + std::optional acceptor; +}; + +/** + * There are situations where we can not know in advance how many sockets we + * need. The main example of this are VST2 `dispatcher()` and `audioMaster()` + * calls. These functions can be called from multiple threads at the same time, + * so using a single socket with a mutex to prevent two threads from using the + * socket at the same time would cause issues. Luckily situation does not come + * up that often so to work around it, we'll do two things: + * + * - We'll keep a single long lived socket connection. This works the exact same + * way as every other `SocketHandler` socket. When we want to send data and + * the socket is primary socket is not currently being written to, we'll just + * use that. On the listening side we'll read from this in a loop. + * - On the listening side we also have a second thread asynchronously listening + * for new connections on the socket endpoint. When the sending side wants to + * send data and the primary socket is in use, it will instantiate a new + * connection to same socket endpoint and it will send the data over that + * socket instead. On the listening side the new connection will be accepted, + * and a newly spawned thread will handle incoming connection just like it + * would for the primary socket. + * + * @tparam Thread The thread implementation to use. On the Linux side this + * should be `std::jthread` and on the Wine side this should be `Win32Thread`. + */ +template +class AdHocSocketHandler { + protected: + /** + * Sets up a single primary socket. The sockets won't be active until + * `connect()` gets called. + * + * @param io_context The IO context the primary socket should be bound to. A + * new IO context will be created for accepting the additional incoming + * connections. + * @param endpoint The socket endpoint used for this event handler. + * @param listen If `true`, start listening on the sockets. Incoming + * connections will be accepted when `connect()` gets called. This should + * be set to `true` on the plugin side, and `false` on the Wine host side. + * + * @see Sockets::connect + */ + AdHocSocketHandler(boost::asio::io_context& io_context, + boost::asio::local::stream_protocol::endpoint endpoint, + bool listen) + : io_context(io_context), endpoint(endpoint), socket(io_context) { + if (listen) { + boost::filesystem::create_directories( + boost::filesystem::path(endpoint.path()).parent_path()); + acceptor.emplace(io_context, endpoint); + } + } + + public: + /** + * Depending on the value of the `listen` argument passed to the + * constructor, either accept connections made to the sockets on the Linux + * side or connect to the sockets on the Wine side + */ + void connect() { + if (acceptor) { + acceptor->accept(socket); + + // As mentioned in `acceptor's` docstring, this acceptor will be + // recreated in `receive_multi()` on another context, and + // potentially on the other side of the connection in the case + // where we're handling `vst_host_callback` VST2 events + acceptor.reset(); + boost::filesystem::remove(endpoint.path()); + } else { + socket.connect(endpoint); + } + } + + /** + * Close the socket. Both sides that are actively listening will be thrown a + * `boost::system_error` when this happens. + */ + void close() { + // The shutdown can fail when the socket is already closed + boost::system::error_code err; + socket.shutdown( + boost::asio::local::stream_protocol::socket::shutdown_both, err); + socket.close(); + } + + protected: + /** + * Serialize and send an event over a socket. This is used for both the host + * -> plugin 'dispatch' events and the plugin -> host 'audioMaster' host + * callbacks since they follow the same format. See one of those functions + * for details on the parameters and return value of this function. + * + * As described above, if this function is currently being called from + * another thread, then this will create a new socket connection and send + * the event there instead. + * + * @param callback A function that will be called with a reference to a + * socket. This is either the primary `socket`, or a new ad hock socket if + * this function is currently being called from another thread. + * + * @tparam T The return value of F. + * @tparam F A function in the form of + * `T(boost::asio::local::stream_protocol::socket&)`. + */ + template + T send(F callback) { + // XXX: Maybe at some point we should benchmark how often this + // ad hoc socket spawning mechanism gets used. If some hosts + // for instance consistently and repeatedly trigger this then + // we might be able to do some optimizations there. + std::unique_lock lock(write_mutex, std::try_to_lock); + if (lock.owns_lock()) { + // This was used to always block when sending the first message, + // because the other side may not be listening for additional + // connections yet + auto result = callback(socket); + sent_first_event = true; + + return result; + } else { + try { + boost::asio::local::stream_protocol::socket secondary_socket( + io_context); + secondary_socket.connect(endpoint); + + return callback(secondary_socket); + } catch (const boost::system::system_error& e) { + // So, what do we do when noone is listening on the endpoint + // yet? This can happen with plugin groups when the Wine + // host process does an `audioMaster()` call before the + // plugin is listening. If that happens we'll fall back to a + // synchronous request. This is not very pretty, so if + // anyone can think of a better way to structure all of this + // while still mainting a long living primary socket please + // let me know. + // Note that this should **only** be done before the call to + // `connect()`. If we get here at any other point then it + // means that the plugin side is no longer listening on the + // sockets, and we should thus just exit. + if (!sent_first_event) { + std::lock_guard lock(write_mutex); + + auto result = callback(socket); + sent_first_event = true; + + return result; + } else { + // Rethrow the exception if the sockets we're not + // handling the specific case described above + throw e; + } + } + } + } + + /** + * Spawn a new thread to listen for extra connections to `endpoint`, and + * then a blocking loop that handles incoming data from the primary + * `socket`. + * + * @param logger A logger instance for logging connection errors. This + * should only be passed on the plugin side. + * @param primary_callback A function that will do a single read cycle for + * the primary socket socket that should do a single read cycle. This is + * called in a loop so it shouldn't do any looping itself. + * @param secondary_callback A function that will be called when we receive + * an incoming connection on a secondary socket. This would often do the + * same thing as `primary_callback`, but secondary sockets may need some + * different handling. + * + * @tparam F A function type in the form of + * `void(boost::asio::local::stream_protocol::socket&)`. + * @tparam G The same as `F`. + */ + template + void receive_multi(std::optional> logger, + F primary_callback, + G secondary_callback) { + // As described above we'll handle incoming requests for `socket` on + // this thread. We'll also listen for incoming connections on `endpoint` + // on another thread. For any incoming connection we'll spawn a new + // thread to handle the request. When `socket` closes and this loop + // breaks, the listener and any still active threads will be cleaned up + // before this function exits. + boost::asio::io_context secondary_context{}; + + // The previous acceptor has already been shut down by + // `AdHocSocketHandler::connect()` + acceptor.emplace(secondary_context, endpoint); + + // This works the exact same was as `active_plugins` and + // `next_plugin_id` in `GroupBridge` + std::map active_secondary_requests{}; + std::atomic_size_t next_request_id{}; + std::mutex active_secondary_requests_mutex{}; + accept_requests( + *acceptor, logger, + [&](boost::asio::local::stream_protocol::socket secondary_socket) { + const size_t request_id = next_request_id.fetch_add(1); + + // We have to make sure to keep moving these sockets into the + // threads that will handle them + std::lock_guard lock(active_secondary_requests_mutex); + active_secondary_requests[request_id] = Thread( + [&, request_id](boost::asio::local::stream_protocol::socket + secondary_socket) { + secondary_callback(secondary_socket); + + // When we have processed this request, we'll join the + // thread again with the thread that's handling + // `secondary_context` + boost::asio::post(secondary_context, [&, request_id]() { + std::lock_guard lock( + active_secondary_requests_mutex); + + // The join is implicit because we're using + // `std::jthread`/`Win32Thread` + active_secondary_requests.erase(request_id); + }); + }, + std::move(secondary_socket)); + }); + + Thread secondary_requests_handler([&]() { secondary_context.run(); }); + + // Now we'll handle reads on the primary socket in a loop until the + // socket shuts down + while (true) { + try { + primary_callback(socket); + } catch (const boost::system::system_error&) { + // This happens when the sockets got closed because the plugin + // is being shut down + break; + } + } + + // After the primary socket gets terminated (during shutdown) we'll make + // sure all outstanding jobs have been processed and then drop all work + // from the IO context + std::lock_guard lock(active_secondary_requests_mutex); + secondary_context.stop(); + acceptor.reset(); + } + + /** + * The same as the above, but with a single callback for incoming + * connections on the primary socket and on secondary sockets. + * + * @overload + */ + template + void receive_multi(std::optional> logger, + F callback) { + receive_multi(logger, callback, callback); + } + + private: + /** + * Used in `receive_multi()` to asynchronously listen for secondary socket + * connections. After `callback()` returns this function will continue to be + * called until the IO context gets stopped. + * + * @param acceptor The acceptor we will be listening on. + * @param logger A logger instance for logging connection errors. This + * should only be passed on the plugin side. + * @param callback A function that handles the new socket connection. + * + * @tparam F A function in the form + * `void(boost::asio::local::stream_protocol::socket)` to handle a new + * incoming connection. + */ + template + void accept_requests( + boost::asio::local::stream_protocol::acceptor& acceptor, + std::optional> logger, + F callback) { + acceptor.async_accept( + [&, logger, callback]( + const boost::system::error_code& error, + boost::asio::local::stream_protocol::socket secondary_socket) { + if (error.failed()) { + // On the Wine side it's expected that the primary socket + // connection will be dropped during shutdown, so we can + // silently ignore any related socket errors on the Wine + // side + if (logger) { + logger->get().log( + "Failure while accepting connections: " + + error.message()); + } + + return; + } + + callback(std::move(secondary_socket)); + + accept_requests(acceptor, logger, callback); + }); + } + + /** + * The main IO context. New sockets created during `send()` will be + * bound to this context. In `receive_multi()` we'll create a new IO context + * since we want to do all listening there on a dedicated thread. + */ + boost::asio::io_context& io_context; + + boost::asio::local::stream_protocol::endpoint endpoint; + boost::asio::local::stream_protocol::socket socket; + + /** + * This acceptor will be used once synchronously on the listening side + * during `Sockets::connect()`. When `AdHocSocketHandler::receive_multi()` + * is then called, we'll recreate the acceptor to asynchronously listen for + * new incoming socket connections on `endpoint` using. This is important, + * because on the case of `Vst2Sockets`'s' `vst_host_callback` the acceptor + * is first accepts an initial socket on the plugin side (like all sockets), + * but all additional incoming connections of course have to be listened for + * on the plugin side. + */ + std::optional acceptor; + + /** + * A mutex that locks the primary `socket`. If this is locked, then any new + * events will be sent over a new socket instead. + */ + std::mutex write_mutex; + + /** + * Indicates whether or not the remove has processed an event we sent from + * this side. When a Windows VST2 plugin performs a host callback in its + * constructor, before the native plugin has had time to connect to the + * sockets, we want it to always wait for the sockets to come online, but + * this fallback behaviour should only happen during initialization. + */ + std::atomic_bool sent_first_event = false; +}; diff --git a/src/common/communication.cpp b/src/common/communication/vst2.cpp similarity index 70% rename from src/common/communication.cpp rename to src/common/communication/vst2.cpp index 1ad60440..da13669d 100644 --- a/src/common/communication.cpp +++ b/src/common/communication/vst2.cpp @@ -14,19 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include "communication.h" - -#include - -#include "utils.h" - -namespace fs = boost::filesystem; - -/** - * Used for generating random identifiers. - */ -constexpr char alphanumeric_characters[] = - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +#include "vst2.h" EventPayload DefaultDataConverter::read(const int /*opcode*/, const int /*index*/, @@ -80,28 +68,3 @@ intptr_t DefaultDataConverter::return_value(const int /*opcode*/, const intptr_t original) const { return original; } - -boost::filesystem::path generate_endpoint_base(const std::string& plugin_name) { - fs::path temp_directory = get_temporary_directory(); - - std::random_device random_device; - std::mt19937 rng(random_device()); - fs::path candidate_endpoint; - do { - std::string random_id; - std::sample( - alphanumeric_characters, - alphanumeric_characters + strlen(alphanumeric_characters) - 1, - std::back_inserter(random_id), 8, rng); - - // We'll get rid of the file descriptors immediately after accepting the - // sockets, so putting them inside of a subdirectory would only leave - // behind an empty directory - std::ostringstream socket_name; - socket_name << "yabridge-" << plugin_name << "-" << random_id; - - candidate_endpoint = temp_directory / socket_name.str(); - } while (fs::exists(candidate_endpoint)); - - return candidate_endpoint; -} diff --git a/src/common/communication/vst2.h b/src/common/communication/vst2.h new file mode 100644 index 00000000..60bb1e95 --- /dev/null +++ b/src/common/communication/vst2.h @@ -0,0 +1,537 @@ +// 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 . + +#pragma once + +#include + +#include "../logging/vst2.h" +#include "../serialization/vst2.h" +#include "common.h" + +/** + * Encodes the base behavior for reading from and writing to the `data` argument + * for event dispatch functions. This provides base functionality for these + * kinds of events. The `dispatch()` function will require some more specific + * structs. + */ +class DefaultDataConverter { + public: + virtual ~DefaultDataConverter(){}; + + /** + * Read data from the `data` void pointer into a an `EventPayload` value + * that can be serialized and conveys the meaning of the event. + */ + virtual EventPayload read(const int opcode, + const int index, + const intptr_t value, + const void* data) const; + + /** + * Read data from the `value` pointer into a an `EventPayload` value that + * can be serialized and conveys the meaning of the event. This is only used + * for the `effSetSpeakerArrangement` and `effGetSpeakerArrangement` events. + */ + virtual std::optional read_value(const int opcode, + const intptr_t value) const; + + /** + * Write the reponse back to the `data` pointer. + */ + virtual void write(const int opcode, + void* data, + const EventResult& response) const; + + /** + * Write the reponse back to the `value` pointer. This is only used during + * the `effGetSpeakerArrangement` event. + */ + virtual void write_value(const int opcode, + intptr_t value, + const EventResult& response) const; + + /** + * This function can override a callback's return value based on the opcode. + * This is used in one place to return a pointer to a `VstTime` object + * that's contantly being updated. + * + * @param opcode The opcode for the current event. + * @param original The original return value as returned by the callback + * function. + */ + virtual intptr_t return_value(const int opcode, + const intptr_t original) const; +}; + +/** + * An instance of `AdHocSocketHandler` that can handle VST2 `dispatcher()` and + * `audioMaster()` events. + * + * For most of our sockets we can just send out our messages on the writing + * side, and do a simple blocking loop on the reading side. The `dispatch()` and + * `audioMaster()` calls are different. Not only do they have they come with + * complex payload values, they can also be called simultaneously from multiple + * threads, and `audioMaster()` and `dispatch()` calls can even be mutually + * recursive. Luckily this does not happen very often, but it does mean that our + * simple 'one-socket-per-function' model doesn't work anymore. Because setting + * up new sockets is quite expensive and this is seldom needed, this works + * slightly differently: + * + * - We'll keep a single long lived socket connection. This works the exact same + * way as every other socket defined in the `Vst2Sockets` class. + * - Aside from that the listening side will have a second thread asynchronously + * listening for new connections on the socket endpoint. + * + * The `EventHandler::send_event()` method is used to send events. If the socket + * is currently being written to, we'll first create a new socket connection as + * described above. Similarly, the `EventHandler::receive_events()` method first + * sets up asynchronous listeners for the socket endpoint, and then block and + * handle events until the main socket is closed. + * + * @tparam Thread The thread implementation to use. On the Linux side this + * should be `std::jthread` and on the Wine side this should be `Win32Thread`. + */ +template +class EventHandler : public AdHocSocketHandler { + public: + /** + * Sets up a single main socket for this type of events. The sockets won't + * be active until `connect()` gets called. + * + * @param io_context The IO context the main socket should be bound to. A + * new IO context will be created for accepting the additional incoming + * connections. + * @param endpoint The socket endpoint used for this event handler. + * @param listen If `true`, start listening on the sockets. Incoming + * connections will be accepted when `connect()` gets called. This should + * be set to `true` on the plugin side, and `false` on the Wine host side. + * + * @see Sockets::connect + */ + EventHandler(boost::asio::io_context& io_context, + boost::asio::local::stream_protocol::endpoint endpoint, + bool listen) + : AdHocSocketHandler(io_context, endpoint, listen) {} + + /** + * Serialize and send an event over a socket. This is used for both the host + * -> plugin 'dispatch' events and the plugin -> host 'audioMaster' host + * callbacks since they follow the same format. See one of those functions + * for details on the parameters and return value of this function. + * + * As described above, if this function is currently being called from + * another thread, then this will create a new socket connection and send + * the event there instead. + * + * @param data_converter Some struct that knows how to read data from and + * write data back to the `data` void pointer. For host callbacks this + * parameter contains either a string or a null pointer while `dispatch()` + * calls might contain opcode specific structs. See the documentation for + * `EventPayload` for more information. The `DefaultDataConverter` defined + * above handles the basic behavior that's sufficient for host callbacks. + * @param logging A pair containing a logger instance and whether or not + * this is for sending `dispatch()` events or host callbacks. Optional + * since it doesn't have to be done on both sides. + * + * @relates EventHandler::receive_events + * @relates passthrough_event + */ + template + intptr_t send_event(D& data_converter, + std::optional> logging, + int opcode, + int index, + intptr_t value, + void* data, + float option) { + // Encode the right payload types for this event. Check the + // documentation for `EventPayload` for more information. These types + // are converted to C-style data structures in `passthrough_event()` so + // they can be passed to a plugin or callback function. + const EventPayload payload = + data_converter.read(opcode, index, value, data); + const std::optional value_payload = + data_converter.read_value(opcode, value); + + if (logging) { + auto [logger, is_dispatch] = *logging; + logger.log_event(is_dispatch, opcode, index, value, payload, option, + value_payload); + } + + const Event event{.opcode = opcode, + .index = index, + .value = value, + .option = option, + .payload = payload, + .value_payload = value_payload}; + + // A socket only handles a single request at a time as to prevent + // messages from arriving out of order. `AdHocSocketHandler::send()` + // will either use a long-living primary socket, or if that's currently + // in use it will spawn a new socket for us. + EventResult response = this->template send( + [&](boost::asio::local::stream_protocol::socket& socket) { + write_object(socket, event); + return read_object(socket); + }); + + if (logging) { + auto [logger, is_dispatch] = *logging; + logger.log_event_response(is_dispatch, opcode, + response.return_value, response.payload, + response.value_payload); + } + + data_converter.write(opcode, data, response); + data_converter.write_value(opcode, value, response); + + return data_converter.return_value(opcode, response.return_value); + } + + /** + * Spawn a new thread to listen for extra connections to `endpoint`, and + * then start a blocking loop that handles events from the primary `socket`. + * + * The specified function will be used to create an `EventResult` from an + * `Event`. This is almost uses `passthrough_event()`, which converts a + * `EventPayload` into the format used by VST2, calls either `dispatch()` or + * `audioMaster()` depending on the context, and then serializes the result + * back into an `EventResultPayload`. + * + * @param logging A pair containing a logger instance and whether or not + * this is for sending `dispatch()` events or host callbacks. Optional + * since it doesn't have to be done on both sides. + * @param callback The function used to generate a response out of an event. + * See the definition of `F` for more information. + * + * @tparam F A function type in the form of `EventResponse(Event, bool)`. + * The boolean flag is `true` when this event was received on the main + * socket, and `false` otherwise. + * + * @relates EventHandler::send_event + * @relates passthrough_event + */ + template + void receive_events(std::optional> logging, + F callback) { + // Reading, processing, and writing back event data from the sockets + // works in the same way regardless of which socket we're using + const auto process_event = + [&](boost::asio::local::stream_protocol::socket& socket, + bool on_main_thread) { + auto event = read_object(socket); + if (logging) { + auto [logger, is_dispatch] = *logging; + logger.log_event(is_dispatch, event.opcode, event.index, + event.value, event.payload, event.option, + event.value_payload); + } + + EventResult response = callback(event, on_main_thread); + if (logging) { + auto [logger, is_dispatch] = *logging; + logger.log_event_response( + is_dispatch, event.opcode, response.return_value, + response.payload, response.value_payload); + } + + write_object(socket, response); + }; + + this->receive_multi( + logging ? std::optional(std::ref(logging->first.logger)) + : std::nullopt, + [&](boost::asio::local::stream_protocol::socket& socket) { + process_event(socket, true); + }, + [&](boost::asio::local::stream_protocol::socket& socket) { + process_event(socket, false); + }); + } +}; + +/** + * Manages all the sockets used for communicating between the plugin and the + * Wine host when hosting a VST2 plugin. + * + * On the plugin side this class should be initialized with `listen` set to + * `true` before launching the Wine VST host. This will start listening on the + * sockets, and the call to `connect()` will then accept any incoming + * connections. + * + * @tparam Thread The thread implementation to use. On the Linux side this + * should be `std::jthread` and on the Wine side this should be `Win32Thread`. + */ +template +class Vst2Sockets : public Sockets { + public: + /** + * Sets up the sockets using the specified base directory. The sockets won't + * be active until `connect()` gets called. + * + * @param io_context The IO context the sockets should be bound to. Relevant + * when doing asynchronous operations. + * @param endpoint_base_dir The base directory that will be used for the + * Unix domain sockets. + * @param listen If `true`, start listening on the sockets. Incoming + * connections will be accepted when `connect()` gets called. This should + * be set to `true` on the plugin side, and `false` on the Wine host side. + * + * @see Vst2Sockets::connect + */ + Vst2Sockets(boost::asio::io_context& io_context, + const boost::filesystem::path& endpoint_base_dir, + bool listen) + : Sockets(endpoint_base_dir), + host_vst_dispatch(io_context, + (base_dir / "host_vst_dispatch.sock").string(), + listen), + vst_host_callback(io_context, + (base_dir / "vst_host_callback.sock").string(), + listen), + host_vst_parameters(io_context, + (base_dir / "host_vst_parameters.sock").string(), + listen), + host_vst_process_replacing( + io_context, + (base_dir / "host_vst_process_replacing.sock").string(), + listen), + host_vst_control(io_context, + (base_dir / "host_vst_control.sock").string(), + listen) {} + + ~Vst2Sockets() { close(); } + + void connect() override { + host_vst_dispatch.connect(); + vst_host_callback.connect(); + host_vst_parameters.connect(); + host_vst_process_replacing.connect(); + host_vst_control.connect(); + } + + void close() override { + // Manually close all sockets so we break out of any blocking operations + // that may still be active + host_vst_dispatch.close(); + vst_host_callback.close(); + host_vst_parameters.close(); + host_vst_process_replacing.close(); + host_vst_control.close(); + } + + // The naming convention for these sockets is `__`. For + // instance the socket named `host_vst_dispatch` forwards + // `AEffect.dispatch()` calls from the native VST host to the Windows VST + // plugin (through the Wine VST host). + + /** + * The socket that forwards all `dispatcher()` calls from the VST host to + * the plugin. + */ + EventHandler host_vst_dispatch; + /** + * The socket that forwards all `audioMaster()` calls from the Windows VST + * plugin to the host. + */ + EventHandler vst_host_callback; + /** + * Used for both `getParameter` and `setParameter` since they mostly + * overlap. + */ + SocketHandler host_vst_parameters; + /** + * Used for processing audio usign the `process()`, `processReplacing()` and + * `processDoubleReplacing()` functions. + */ + SocketHandler host_vst_process_replacing; + /** + * A control socket that sends data that is not suitable for the other + * sockets. At the moment this is only used to, on startup, send the Windows + * VST plugin's `AEffect` object to the native VST plugin, and to then send + * the configuration (from `config`) back to the Wine host. + */ + SocketHandler host_vst_control; +}; + +/** + * Unmarshall an `EventPayload` back to the representation used by VST2, pass + * that value to a callback function (either `AEffect::dispatcher()` for host -> + * plugin events or `audioMaster()` for plugin -> host events), and then + * serialize the results back into an `EventResult`. + * + * This is the receiving analogue of the `*DataCovnerter` objects. + * + * @param plugin The `AEffect` instance that should be passed to the callback + * function. During `WantsAEffect` we'll send back a copy of this, and when we + * get sent an `AEffect` instance (e.g. during `audioMasterIOChanged()`) we'll + * write the updated values to this instance. + * @param callback The function to call with the arguments received from the + * socket, either `AEffect::dispatcher()` or `audioMasterCallback()`. + * + * @tparam F A function with the same signature as `AEffect::dispatcher` or + * `audioMasterCallback`. + * + * @return The result of the operation. If necessary the `DataConverter` will + * unmarshall the payload again and write it back. + * + * @relates EventHandler::receive_events + */ +template +EventResult passthrough_event(AEffect* plugin, F callback, Event& event) { + // This buffer is used to write strings and small objects to. We'll + // initialize the beginning with null values to both prevent it from being + // read as some arbitrary C-style string, and to make sure that + // `*static_cast(string_buffer.data)` will be a null pointer if the + // plugin is supposed to write a pointer there but doesn't (such as with + // `effEditGetRect`/`WantsVstRect`). + std::array string_buffer; + std::fill(string_buffer.begin(), string_buffer.begin() + sizeof(size_t), 0); + + auto read_payload_fn = overload{ + [&](const std::nullptr_t&) -> void* { return nullptr; }, + [&](const std::string& s) -> void* { + return const_cast(s.c_str()); + }, + [&](const ChunkData& chunk) -> void* { + return const_cast(chunk.buffer.data()); + }, + [&](native_size_t& window_handle) -> void* { + // This is the X11 window handle that the editor should reparent + // itself to. We have a special wrapper around the dispatch function + // that intercepts `effEditOpen` events and creates a Win32 window + // and then finally embeds the X11 window Wine created into this + // wnidow handle. Make sure to convert the window ID first to + // `size_t` in case this is the 32-bit host. + return reinterpret_cast(static_cast(window_handle)); + }, + [&](const AEffect&) -> void* { return nullptr; }, + [&](DynamicVstEvents& events) -> void* { + return &events.as_c_events(); + }, + [&](DynamicSpeakerArrangement& speaker_arrangement) -> void* { + return &speaker_arrangement.as_c_speaker_arrangement(); + }, + [&](WantsAEffectUpdate&) -> void* { + // The host will never actually ask for an updated `AEffect` object + // since that should not be a thing. This is purely a meant as a + // workaround for plugins that initialize their `AEffect` object + // after the plugin has already finished initializing. + return nullptr; + }, + [&](WantsChunkBuffer&) -> void* { return string_buffer.data(); }, + [&](VstIOProperties& props) -> void* { return &props; }, + [&](VstMidiKeyName& key_name) -> void* { return &key_name; }, + [&](VstParameterProperties& props) -> void* { return &props; }, + [&](WantsVstRect&) -> void* { return string_buffer.data(); }, + [&](const WantsVstTimeInfo&) -> void* { return nullptr; }, + [&](WantsString&) -> void* { return string_buffer.data(); }}; + + // Almost all events pass data through the `data` argument. There are two + // events, `effSetParameter` and `effGetParameter` that also pass data + // through the value argument. + void* data = std::visit(read_payload_fn, event.payload); + intptr_t value = event.value; + if (event.value_payload) { + value = reinterpret_cast( + std::visit(read_payload_fn, *event.value_payload)); + } + + const intptr_t return_value = + callback(plugin, event.opcode, event.index, value, data, event.option); + + // Only write back data when needed, this depends on the event payload type + auto write_payload_fn = overload{ + [&](auto) -> EventResultPayload { return nullptr; }, + [&](const AEffect& updated_plugin) -> EventResultPayload { + // This is a bit of a special case! Instead of writing some return + // value, we will update values on the native VST plugin's `AEffect` + // object. This is triggered by the `audioMasterIOChanged` callback + // from the hosted VST plugin. + update_aeffect(*plugin, updated_plugin); + + return nullptr; + }, + [&](DynamicSpeakerArrangement& speaker_arrangement) + -> EventResultPayload { return speaker_arrangement; }, + [&](WantsAEffectUpdate&) -> EventResultPayload { return *plugin; }, + [&](WantsChunkBuffer&) -> EventResultPayload { + // In this case the plugin will have written its data stored in an + // array to which a pointer is stored in `data`, with the return + // value from the event determines how much data the plugin has + // written + const uint8_t* chunk_data = *static_cast(data); + return ChunkData{ + std::vector(chunk_data, chunk_data + return_value)}; + }, + [&](WantsVstRect&) -> EventResultPayload { + // The plugin should have written a pointer to a VstRect struct into + // the data pointer. I haven't seen this fail yet, but since some + // hosts will call `effEditGetRect()` before `effEditOpen()` I can + // assume there are plugins that don't handle this correctly. + VstRect* editor_rect = *static_cast(data); + if (!editor_rect) { + return nullptr; + } + + return *editor_rect; + }, + [&](WantsVstTimeInfo&) -> EventResultPayload { + // Not sure why the VST API has twenty different ways of + // returning structs, but in this case the value returned from + // the callback function is actually a pointer to a + // `VstTimeInfo` struct! It can also be a null pointer if the + // host doesn't support this. + const auto time_info = + reinterpret_cast(return_value); + if (!time_info) { + return nullptr; + } else { + return *time_info; + } + }, + [&](WantsString&) -> EventResultPayload { + return std::string(static_cast(data)); + }, + [&](VstIOProperties& props) -> EventResultPayload { return props; }, + [&](VstMidiKeyName& key_name) -> EventResultPayload { + return key_name; + }, + [&](VstParameterProperties& props) -> EventResultPayload { + return props; + }}; + + // As mentioned about, the `effSetSpeakerArrangement` and + // `effGetSpeakerArrangement` events are the only two events that use the + // value argument as a pointer to write data to. Additionally, the + // `effGetSpeakerArrangement` expects the plugin to write its own data to + // this value. Hence why we need to encode the response here separately. + const EventResultPayload response_data = + std::visit(write_payload_fn, event.payload); + std::optional value_response_data = std::nullopt; + if (event.value_payload) { + value_response_data = + std::visit(write_payload_fn, *event.value_payload); + } + + EventResult response{.return_value = return_value, + .payload = response_data, + .value_payload = value_response_data}; + + return response; +} diff --git a/src/common/communication/vst3.h b/src/common/communication/vst3.h new file mode 100644 index 00000000..e306c333 --- /dev/null +++ b/src/common/communication/vst3.h @@ -0,0 +1,471 @@ +// 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 . + +#pragma once + +#include +#include + +#include "../logging/vst3.h" +#include "../serialization/vst3.h" +#include "common.h" + +/** + * An instance of `AdHocSocketHandler` that encapsulates the simple + * communication model we use for sending requests and receiving responses. A + * request of type `T`, where `T` is in `{Control,Callback}Request`, should be + * answered with an object of type `T::Response`. + * + * See the docstrings on `EventHandler` and `AdHocSocketHandler` for more + * information on how this works internally and why it works the way it does. + * + * @note The name of this class is not to be confused with VST3's `IMessage` as + * this is very much just general purpose messaging between yabridge's two + * components. Of course, this will handle `IMessage` function calls as well. + * + * @tparam Thread The thread implementation to use. On the Linux side this + * should be `std::jthread` and on the Wine side this should be `Win32Thread`. + * @tparam Request Either `ControlRequest` or `CallbackRequest`. + */ +template +class Vst3MessageHandler : public AdHocSocketHandler { + public: + /** + * Sets up a single main socket for this type of events. The sockets won't + * be active until `connect()` gets called. + * + * @param io_context The IO context the main socket should be bound to. A + * new IO context will be created for accepting the additional incoming + * connections. + * @param endpoint The socket endpoint used for this event handler. + * @param listen If `true`, start listening on the sockets. Incoming + * connections will be accepted when `connect()` gets called. This should + * be set to `true` on the plugin side, and `false` on the Wine host side. + * + * @see Sockets::connect + */ + Vst3MessageHandler(boost::asio::io_context& io_context, + boost::asio::local::stream_protocol::endpoint endpoint, + bool listen) + : AdHocSocketHandler(io_context, endpoint, listen) {} + + /** + * Serialize and send an event over a socket and return the appropriate + * response. + * + * As described above, if this function is currently being called from + * another thread, then this will create a new socket connection and send + * the event there instead. + * + * @param object The request object to send. Often a marker struct to ask + * for a specific object to be returned. + * @param logging A pair containing a logger instance and whether or not + * this is for sending host -> plugin control messages. If set to false, + * then this indicates that this `Vst3MessageHandler` is handling plugin + * -> host callbacks isntead. Optional since it only has to be set on the + * plugin's side. + * @param buffer The serialization and receiving buffer to reuse. This is + * optional, but it's useful for minimizing allocations in the audio + * processing loop. + * + * @relates Vst3MessageHandler::receive_messages + */ + template + typename T::Response send_message( + const T& object, + std::optional> logging, + std::vector& buffer) { + typename T::Response response_object; + receive_into(object, response_object, logging, buffer); + + return response_object; + } + + /** + * The same as the above, but with a small default buffer. + * + * @overload + */ + template + typename T::Response send_message( + const T& object, + std::optional> logging) { + typename T::Response response_object; + receive_into(object, response_object, logging); + + return response_object; + } + + /** + * `Vst3MessageHandler::send_message()`, but deserializing the response into + * an existing object. + * + * @param response_object The object to deserialize into. + * + * @overload Vst3MessageHandler::send_message + */ + template + typename T::Response& receive_into( + const T& object, + typename T::Response& response_object, + std::optional> logging, + std::vector& buffer) { + using TResponse = typename T::Response; + + // Since a lot of messages just return a `tresult`, we can't filter out + // responses based on the response message type. Instead, we'll just + // only print the responses when the request was not filtered out. + bool should_log_response = false; + if (logging) { + auto [logger, is_host_vst] = *logging; + should_log_response = logger.log_request(is_host_vst, object); + } + + // A socket only handles a single request at a time as to prevent + // messages from arriving out of order. `AdHocSocketHandler::send()` + // will either use a long-living primary socket, or if that's currently + // in use it will spawn a new socket for us. + this->template send( + [&](boost::asio::local::stream_protocol::socket& socket) { + write_object(socket, Request(object), buffer); + read_object(socket, buffer, response_object); + // FIXME: We have to return something here, and ML was not yet + // invented when they came up with C++ so void is not + // valid here + return std::monostate{}; + }); + + if (should_log_response) { + auto [logger, is_host_vst] = *logging; + logger.log_response(!is_host_vst, response_object); + } + + return response_object; + } + + /** + * The same function as above, but with a small default buffer. + * + * @overload + */ + template + typename T::Response& receive_into( + const T& object, + typename T::Response& response_object, + std::optional> logging) { + std::vector buffer(64); + return receive_into(object, response_object, std::move(logging), + buffer); + } + + /** + * Spawn a new thread to listen for extra connections to `endpoint`, and + * then start a blocking loop that handles messages from the primary + * `socket`. + * + * The specified function receives a `Request` variant object containing an + * object of type `T`, and it should then return the corresponding + * `T::Response`. + * + * @param logging A pair containing a logger instance and whether or not + * this is for sending host -> plugin control messages. If set to false, + * then this indicates that this `Vst3MessageHandler` is handling plugin + * -> host callbacks isntead. Optional since it only has to be set on the + * plugin's side. + * @param callback The function used to generate a response out of the + * request. See the definition of `F` for more information. + * + * @tparam F A function type in the form of `T::Response(T)` for every `T` + * in `Request`. This way we can directly deserialize into a `T::Response` + * on the side that called `receive_into(T, T::Response&)`. + * @tparam persistent_buffers Whether processing buffers should be kept + * around and reused. This is used to minimize allocations in the audio + * processing loop. These buffers will also never shrink, but that should + * not be an issue with the `IAudioProcessor` and `IComponent` functions. + * Saving and loading state is handled on the main sockets. + * + * @relates Vst3MessageHandler::send_event + */ + template + void receive_messages(std::optional> logging, + F callback) { + thread_local std::vector persistent_buffer{}; + + // Reading, processing, and writing back the response for the requests + // we receive works in the same way regardless of which socket we're + // using + const auto process_message = + [&](boost::asio::local::stream_protocol::socket& socket) { + auto request = + persistent_buffers + ? read_object(socket, persistent_buffer) + : read_object(socket); + + // See the comment in `receive_into()` for more information + bool should_log_response = false; + if (logging) { + should_log_response = std::visit( + [&](const auto& object) { + auto [logger, is_host_vst] = *logging; + return logger.log_request(is_host_vst, object); + }, + request); + } + + // We do the visiting here using a templated lambda. This way we + // always know for sure that the function returns the correct + // type, and we can scrap a lot of boilerplate elsewhere. + std::visit( + [&](T object) { + typename T::Response response = callback(object); + + if (should_log_response) { + auto [logger, is_host_vst] = *logging; + logger.log_response(!is_host_vst, response); + } + + if constexpr (persistent_buffers) { + write_object(socket, response, persistent_buffer); + } else { + write_object(socket, response); + } + }, + request); + }; + + this->receive_multi(logging + ? std::optional(std::ref(logging->first.logger)) + : std::nullopt, + process_message); + } +}; + +/** + * Manages all the sockets used for communicating between the plugin and the + * Wine host when hosting a VST3 plugin. + * + * On the plugin side this class should be initialized with `listen` set to + * `true` before launching the Wine VST host. This will start listening on the + * sockets, and the call to `connect()` will then accept any incoming + * connections. + * + * We'll have a host -> plugin connection for sending control messages (which is + * just a made up term to more easily differentiate between the two directions), + * and a plugin -> host connection to allow the plugin to make callbacks. Both + * of these connections are capable of spawning additional sockets and threads + * as needed. + * + * For audio processing (or anything that implement `IAudioProcessor` or + * `IComonent`) we'll use dedicated sockets per instance, since we don't want to + * do anything that could increase latency there. + * + * @tparam Thread The thread implementation to use. On the Linux side this + * should be `std::jthread` and on the Wine side this should be `Win32Thread`. + */ +template +class Vst3Sockets : public Sockets { + public: + /** + * Sets up the sockets using the specified base directory. The sockets won't + * be active until `connect()` gets called. + * + * @param io_context The IO context the sockets should be bound to. Relevant + * when doing asynchronous operations. + * @param endpoint_base_dir The base directory that will be used for the + * Unix domain sockets. + * @param listen If `true`, start listening on the sockets. Incoming + * connections will be accepted when `connect()` gets called. This should + * be set to `true` on the plugin side, and `false` on the Wine host side. + * + * @see Vst3Sockets::connect + */ + Vst3Sockets(boost::asio::io_context& io_context, + const boost::filesystem::path& endpoint_base_dir, + bool listen) + : Sockets(endpoint_base_dir), + host_vst_control(io_context, + (base_dir / "host_vst_control.sock").string(), + listen), + vst_host_callback(io_context, + (base_dir / "vst_host_callback.sock").string(), + listen), + io_context(io_context) {} + + ~Vst3Sockets() { close(); } + + void connect() override { + host_vst_control.connect(); + vst_host_callback.connect(); + } + + void close() override { + // Manually close all sockets so we break out of any blocking operations + // that may still be active + host_vst_control.close(); + vst_host_callback.close(); + + // This map should be empty at this point, but who knows + std::lock_guard lock(audio_processor_sockets_mutex); + for (auto& [instance_id, socket] : audio_processor_sockets) { + socket.close(); + } + } + + /** + * Connect to the dedicated `IAudioProcessor` and `IConnect` handling socket + * for a plugin object instance. This should be called on the plugin side + * after instantiating such an object. + * + * @param instance_id The object instance identifier of the socket. + */ + void add_audio_processor_and_connect(size_t instance_id) { + std::lock_guard lock(audio_processor_sockets_mutex); + audio_processor_sockets.try_emplace( + instance_id, io_context, + (base_dir / ("host_vst_audio_processor_" + + std::to_string(instance_id) + ".sock")) + .string(), + false); + audio_processor_buffers.try_emplace(instance_id); + + audio_processor_sockets.at(instance_id).connect(); + } + + /** + * Create and listen on a dedicated `IAudioProcessor` and `IConnect` + * handling socket for a plugin object instance. The calling thread will + * block until the socket has been closed. This should be called from the + * Wine plugin host side after instantiating such an object. + * + * @param instance_id The object instance identifier of the socket. + * @param socket_listening_latch A promise we'll set a value for once the + * socket is being listened on so we can wait for it. Otherwise it can be + * that the native plugin already tries to connect to the socket before + * Wine plugin host is even listening on it. + * @param cb An overloaded function that can take every type `T` in the + * `AudioProcessorRequest` variant and then returns `T::Response`. + */ + template + void add_audio_processor_and_listen( + size_t instance_id, + std::promise& socket_listening_latch, + F cb) { + { + std::lock_guard lock(audio_processor_sockets_mutex); + audio_processor_sockets.try_emplace( + instance_id, io_context, + (base_dir / ("host_vst_audio_processor_" + + std::to_string(instance_id) + ".sock")) + .string(), + true); + audio_processor_buffers.try_emplace(instance_id); + } + + socket_listening_latch.set_value(); + audio_processor_sockets.at(instance_id).connect(); + + // This `true` indicates that we want to reuse our serialization and + // receiving buffers for all calls. This slightly reduces the amount of + // allocations in the audio processing loop. + audio_processor_sockets.at(instance_id) + .template receive_messages(std::nullopt, cb); + } + + /** + * If `instance_id` is in `audio_processor_sockets`, then close its socket + * and remove it from the map. This is called from the destructor of + * `Vst3PluginProxyImpl` on the plugin side and when handling + * `Vst3PluginProxy::Destruct` on the Wine plugin host side. + * + * @param instance_id The object instance identifier of the socket. + * + * @return Whether the socket was closed and removed. Returns false if it + * wasn't in the map. + */ + bool remove_audio_processor(size_t instance_id) { + std::lock_guard lock(audio_processor_sockets_mutex); + if (audio_processor_sockets.contains(instance_id)) { + audio_processor_sockets.at(instance_id).close(); + audio_processor_sockets.erase(instance_id); + audio_processor_buffers.erase(instance_id); + + return true; + } else { + return false; + } + } + + /** + * Send a message from the native plugin to the Wine plugin host to handle + * an `IAudioProcessor` or `IComponent` call. Since those functions are + * called from a hot loop we want every instance to have a dedicated socket + * and thread for handling those. These calls also always reuse buffers to + * minimize allocations. + * + * @tparam T Some object in the `AudioProcessorRequest` variant. + */ + template + typename T::Response send_audio_processor_message( + const T& object, + std::optional> logging) { + return audio_processor_sockets.at(object.instance_id) + .send_message(object, logging, + audio_processor_buffers.at(object.instance_id)); + } + + /** + * For sending messages from the host to the plugin. After we have a better + * idea of what our communication model looks like we'll probably want to + * provide an abstraction similar to `EventHandler`. For optimization + * reasons calls to `IAudioProcessor` or `IComponent` are handled using the + * dedicated sockets in `audio_processor_sockets`. + * + * This will be listened on by the Wine plugin host when it calls + * `receive_multi()`. + */ + Vst3MessageHandler host_vst_control; + + /** + * For sending callbacks from the plugin back to the host. After we have a + * better idea of what our communication model looks like we'll probably + * want to provide an abstraction similar to `EventHandler`. + */ + Vst3MessageHandler vst_host_callback; + + private: + boost::asio::io_context& io_context; + + /** + * Every `IAudioProcessor` or `IComponent` instance (which likely implements + * both of those) will get a dedicated socket. These functions are always + * called in a hot loop, so there should not be any waiting or additional + * thread or socket creation happening there. + * + * THe last `false` template arguments means that we'll disable all ad-hoc + * socket and thread spawning behaviour. Otherwise every plugin instance + * would have one dedicated thread for handling function calls to these + * interfaces, and then another dedicated thread just idling around. + */ + std::map> + audio_processor_sockets; + /** + * Binary buffers used for serializing objects and receiving messages into + * during `send_audio_processor_message()`. This is used to minimize the + * amount of allocations in the audio processing loop. + */ + std::map> audio_processor_buffers; + std::mutex audio_processor_sockets_mutex; +}; diff --git a/src/common/configuration.cpp b/src/common/configuration.cpp index 8f528a15..79c6e715 100644 --- a/src/common/configuration.cpp +++ b/src/common/configuration.cpp @@ -90,6 +90,12 @@ Configuration::Configuration(const fs::path& config_path, } else { invalid_options.push_back(key); } + } else if (key == "editor_xembed") { + if (const auto parsed_value = value.as_boolean()) { + editor_xembed = parsed_value->get(); + } else { + invalid_options.push_back(key); + } } else if (key == "group") { if (const auto parsed_value = value.as_string()) { group = parsed_value->get(); diff --git a/src/common/configuration.h b/src/common/configuration.h index 74981337..3b061e3f 100644 --- a/src/common/configuration.h +++ b/src/common/configuration.h @@ -33,8 +33,8 @@ * they can share resources. Configuration file loading works as follows: * * 1. `load_config_for(path)` from `src/plugin/utils.h` gets called with a path - * to the copy of or symlink to `libyabridge.so` that the plugin host has - * tried to load. + * to the copy of or symlink to `libyabridge-{vst2,vst3}.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. @@ -101,6 +101,14 @@ class Configuration { */ bool editor_double_embed = false; + /** + * Use XEmbed instead of yabridge's normal editor embedding method. Wine's + * XEmbed support is not very polished yet and tends to lead to rendering + * issues, so this is disabled by default. Also, editor resizing won't work + * reliably when XEmbed is enabled. + */ + bool editor_xembed = false; + /** * The name of the plugin group that should be used for the plugin this * configuration object was created for. If not set, then the plugin should @@ -135,10 +143,11 @@ class Configuration { void serialize(S& s) { s.value1b(cache_time_info); s.value1b(editor_double_embed); + s.value1b(editor_xembed); s.ext(group, bitsery::ext::StdOptional(), [](S& s, auto& v) { s.text1b(v, 4096); }); s.ext(matched_file, bitsery::ext::StdOptional(), - [](S& s, auto& v) { s.ext(v, bitsery::ext::BoostPath()); }); + [](S& s, auto& v) { s.ext(v, bitsery::ext::BoostPath{}); }); s.ext(matched_pattern, bitsery::ext::StdOptional(), [](S& s, auto& v) { s.text1b(v, 4096); }); diff --git a/src/common/logging/common.cpp b/src/common/logging/common.cpp new file mode 100644 index 00000000..c9877479 --- /dev/null +++ b/src/common/logging/common.cpp @@ -0,0 +1,129 @@ +// 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 . + +#include "common.h" + +#ifdef __WINE__ +#include "../wine-host/boost-fix.h" +#endif + +#include + +#include +#include +#include +#include +#include +#include + +/** + * The environment variable indicating whether to log to a file. Will log to + * STDERR if not specified. + */ +constexpr char logging_file_environment_variable[] = "YABRIDGE_DEBUG_FILE"; + +/** + * The verbosity of the logging, defaults to `Logger::Verbosity::basic`. + * + * @see Logger::Verbosity + */ +constexpr char logging_verbosity_environment_variable[] = + "YABRIDGE_DEBUG_LEVEL"; + +Logger::Logger(std::shared_ptr stream, + Verbosity verbosity_level, + std::string prefix) + : verbosity(verbosity_level), stream(stream), prefix(prefix) {} + +Logger Logger::create_from_environment(std::string prefix) { + auto env = boost::this_process::environment(); + std::string file_path = env[logging_file_environment_variable].to_string(); + std::string verbosity = + env[logging_verbosity_environment_variable].to_string(); + + // Default to `Verbosity::basic` if the environment variable has not + // been set or if it is not an integer. + Verbosity verbosity_level; + try { + verbosity_level = static_cast(std::stoi(verbosity)); + } catch (const std::invalid_argument&) { + verbosity_level = Verbosity::basic; + } + + // If `file` points to a valid location then use create/truncate the + // file and write all of the logs there, otherwise use STDERR + auto log_file = std::make_shared( + file_path, std::fstream::out | std::fstream::app); + if (log_file->is_open()) { + return Logger(log_file, verbosity_level, prefix); + } else { + // 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("/dev/stderr"), + verbosity_level, prefix); + } +} + +Logger Logger::create_wine_stderr() { + auto env = boost::this_process::environment(); + std::string verbosity = + env[logging_verbosity_environment_variable].to_string(); + + Verbosity verbosity_level; + try { + verbosity_level = static_cast(std::stoi(verbosity)); + } catch (const std::invalid_argument&) { + verbosity_level = Verbosity::basic; + } + + // We're logging directly to `std::cerr` instead of to `/dev/stderr` because + // we want the STDERR redirection from the group host processes to still + // function here + return Logger(std::shared_ptr(&std::cerr, [](auto*) {}), + verbosity_level, ""); +} + +void Logger::log(const std::string& message) { + const auto current_time = std::chrono::system_clock::now(); + const std::time_t timestamp = + std::chrono::system_clock::to_time_t(current_time); + + // How did C++ manage to get time formatting libraries without a way to + // actually get a timestamp in a threadsafe way? `localtime_r` in C++ is not + // portable but luckily we only have to support GCC anyway. + std::tm tm; + localtime_r(×tamp, &tm); + + std::ostringstream formatted_message; + formatted_message << std::put_time(&tm, "%T") << " "; + formatted_message << prefix; + formatted_message << message; + // Flushing a stringstream doesn't do anything, but we need to put a + // linefeed in this string stream rather writing it sprightly to the output + // stream to prevent two messages from being put on the same row + formatted_message << std::endl; + + *stream << formatted_message.str() << std::flush; +} + +void Logger::log_trace(const std::string& message) { + if (verbosity >= Verbosity::all_events) { + log(message); + } +} diff --git a/src/common/logging.h b/src/common/logging/common.h similarity index 72% rename from src/common/logging.h rename to src/common/logging/common.h index e01075fd..3f10abdc 100644 --- a/src/common/logging.h +++ b/src/common/logging/common.h @@ -16,11 +16,10 @@ #pragma once +#include #include #include -#include "serialization.h" - /** * Super basic logging facility meant for debugging malfunctioning VST * plugins. This is also used to redirect the output of the Wine process @@ -86,6 +85,13 @@ class Logger { */ static Logger create_from_environment(std::string prefix = ""); + /** + * Create a special logger instance that outputs directly to STDERR without + * any prefixes. This is used to be able to log filterable messages from the + * Wine side of things. + */ + static Logger create_wine_stderr(); + /** * Write a message to the log, prefixing it with a timestamp and this * logger's prefix string. @@ -94,29 +100,6 @@ class Logger { */ void log(const std::string& message); - // The following functions are for logging specific events, they are only - // enabled for verbosity levels higher than 1 (i.e. `Verbosity::events`) - void log_get_parameter(int index); - void log_get_parameter_response(float vlaue); - void log_set_parameter(int index, float value); - void log_set_parameter_response(); - // If `is_dispatch` is `true`, then use opcode names from the plugin's - // dispatch function. Otherwise use names for the host callback function - // opcodes. - void log_event(bool is_dispatch, - int opcode, - int index, - intptr_t value, - const EventPayload& payload, - float option, - const std::optional& value_payload); - void log_event_response( - bool is_dispatch, - int opcode, - intptr_t return_value, - const EventResultPayload& payload, - const std::optional& value_payload); - /** * Log a message that should only be printed when the `verbosity` is set to * `all_events`. This should only be used for simple primitive messages @@ -127,38 +110,20 @@ class Logger { */ void log_trace(const std::string& message); - private: /** - * Determine whether an event should be filtered based on the current - * verbosity level. + * The verbosity level of this logger instance. Based on this certain + * messages may or may not be shown. */ - bool should_filter_event(bool is_dispatch, int opcode) const; + const Verbosity verbosity; + private: /** * The output stream to write the log messages to. Typically either STDERR * or a file stream. */ std::shared_ptr stream; - /** - * The verbosity level of this logger instance. Based on this certain - * messages may or may not be shown. - */ - Verbosity verbosity; /** * A prefix that gets prepended before every message. */ std::string prefix; }; - -/** - * Convert an event opcode to a human readable string for debugging purposes. - * See `src/include/vestige/aeffectx.h` for a complete list of these opcodes. - * - * @param is_dispatch Whether to use opcodes for the `dispatch` function. Will - * use the names from the host callback function if set to false. - * @param opcode The opcode of the event. - * - * @return Either the name from `aeffectx.h`, or a nullopt if it was not listed - * there. - */ -std::optional opcode_to_string(bool is_dispatch, int opcode); diff --git a/src/common/logging.cpp b/src/common/logging/vst2.cpp similarity index 80% rename from src/common/logging.cpp rename to src/common/logging/vst2.cpp index 98aa8300..e03c7662 100644 --- a/src/common/logging.cpp +++ b/src/common/logging/vst2.cpp @@ -14,316 +14,11 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include "logging.h" +#include "vst2.h" -#ifdef __WINE__ -#include "../wine-host/boost-fix.h" -#endif +#include -#include - -#include -#include -#include -#include -#include -#include - -#include "vst24.h" - -/** - * The environment variable indicating whether to log to a file. Will log to - * STDERR if not specified. - */ -constexpr char logging_file_environment_variable[] = "YABRIDGE_DEBUG_FILE"; - -/** - * The verbosity of the logging, defaults to `Logger::Verbosity::basic`. - * - * @see Logger::Verbosity - */ -constexpr char logging_verbosity_environment_variable[] = - "YABRIDGE_DEBUG_LEVEL"; - -Logger::Logger(std::shared_ptr stream, - Verbosity verbosity_level, - std::string prefix) - : stream(stream), verbosity(verbosity_level), prefix(prefix) {} - -Logger Logger::create_from_environment(std::string prefix) { - auto env = boost::this_process::environment(); - std::string file_path = env[logging_file_environment_variable].to_string(); - std::string verbosity = - env[logging_verbosity_environment_variable].to_string(); - - // Default to `Verbosity::basic` if the environment variable has not - // been set or if it is not an integer. - Verbosity verbosity_level; - try { - verbosity_level = static_cast(std::stoi(verbosity)); - } catch (const std::invalid_argument&) { - verbosity_level = Verbosity::basic; - } - - // If `file` points to a valid location then use create/truncate the - // file and write all of the logs there, otherwise use STDERR - auto log_file = std::make_shared( - file_path, std::fstream::out | std::fstream::app); - if (log_file->is_open()) { - return Logger(log_file, verbosity_level, prefix); - } else { - // 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("/dev/stderr"), - verbosity_level, prefix); - } -} - -void Logger::log(const std::string& message) { - const auto current_time = std::chrono::system_clock::now(); - const std::time_t timestamp = - std::chrono::system_clock::to_time_t(current_time); - - // How did C++ manage to get time formatting libraries without a way to - // actually get a timestamp in a threadsafe way? `localtime_r` in C++ is not - // portable but luckily we only have to support GCC anyway. - std::tm tm; - localtime_r(×tamp, &tm); - - std::ostringstream formatted_message; - formatted_message << std::put_time(&tm, "%T") << " "; - formatted_message << prefix; - formatted_message << message; - // Flushing a stringstream doesn't do anything, but we need to put a - // linefeed in this string stream rather writing it sprightly to the output - // stream to prevent two messages from being put on the same row - formatted_message << std::endl; - - *stream << formatted_message.str() << std::flush; -} - -void Logger::log_trace(const std::string& message) { - if (verbosity >= Verbosity::all_events) { - log(message); - } -} - -void Logger::log_get_parameter(int index) { - if (BOOST_UNLIKELY(verbosity >= Verbosity::most_events)) { - std::ostringstream message; - message << ">> getParameter() " << index; - - log(message.str()); - } -} - -void Logger::log_get_parameter_response(float value) { - if (BOOST_UNLIKELY(verbosity >= Verbosity::most_events)) { - std::ostringstream message; - message << " getParameter() :: " << value; - - log(message.str()); - } -} - -void Logger::log_set_parameter(int index, float value) { - if (BOOST_UNLIKELY(verbosity >= Verbosity::most_events)) { - std::ostringstream message; - message << ">> setParameter() " << index << " = " << value; - - log(message.str()); - } -} - -void Logger::log_set_parameter_response() { - if (BOOST_UNLIKELY(verbosity >= Verbosity::most_events)) { - log(" setParameter() :: OK"); - } -} - -void Logger::log_event(bool is_dispatch, - int opcode, - int index, - intptr_t value, - const EventPayload& payload, - float option, - const std::optional& value_payload) { - if (BOOST_UNLIKELY(verbosity >= Verbosity::most_events)) { - if (should_filter_event(is_dispatch, opcode)) { - return; - } - - std::ostringstream message; - if (is_dispatch) { - message << ">> dispatch() "; - } else { - message << ">> audioMasterCallback() "; - } - - const auto opcode_name = opcode_to_string(is_dispatch, opcode); - if (opcode_name) { - message << *opcode_name; - } else { - message << ""; - } - - message << "(index = " << index << ", value = " << value - << ", option = " << option << ", data = "; - - // Only used during `effSetSpeakerArrangement` and - // `effGetSpeakerArrangement` - if (value_payload) { - std::visit( - overload{ - [&](auto) {}, - [&](const DynamicSpeakerArrangement& speaker_arrangement) { - message << "<" << speaker_arrangement.speakers.size() - << " input_speakers>, "; - }}, - *value_payload); - } - - std::visit( - overload{ - [&](const std::nullptr_t&) { message << ""; }, - [&](const std::string& s) { - if (s.size() < 32) { - message << "\"" << s << "\""; - } else { - // Long strings contain binary data that we probably - // don't want to print - message << "<" << s.size() << " bytes>"; - } - }, - [&](const ChunkData& chunk) { - message << "<" << chunk.buffer.size() << " byte chunk>"; - }, - [&](const native_size_t& window_id) { - message << ""; - }, - [&](const AEffect&) { message << ""; }, - [&](const DynamicVstEvents& events) { - message << "<" << events.events.size() << " midi_events>"; - }, - [&](const DynamicSpeakerArrangement& speaker_arrangement) { - message << "<" << speaker_arrangement.speakers.size() - << " output_speakers>"; - }, - [&](const VstIOProperties&) { message << ""; }, - [&](const VstMidiKeyName&) { message << ""; }, - [&](const VstParameterProperties&) { - message << ""; - }, - [&](const WantsAEffectUpdate&) { message << ""; }, - [&](const WantsChunkBuffer&) { - message << ""; - }, - [&](const WantsVstRect&) { message << ""; }, - [&](const WantsVstTimeInfo&) { message << ""; }, - [&](const WantsString&) { message << ""; }}, - payload); - - message << ")"; - - log(message.str()); - } -} - -void Logger::log_event_response( - bool is_dispatch, - int opcode, - intptr_t return_value, - const EventResultPayload& payload, - const std::optional& value_payload) { - if (BOOST_UNLIKELY(verbosity >= Verbosity::most_events)) { - if (should_filter_event(is_dispatch, opcode)) { - return; - } - - std::ostringstream message; - if (is_dispatch) { - message << " dispatch() :: "; - } else { - message << " audioMasterCallback() :: "; - } - - message << return_value; - - // Only used during `effSetSpeakerArrangement` and - // `effGetSpeakerArrangement` - if (value_payload) { - std::visit( - overload{ - [&](auto) {}, - [&](const DynamicSpeakerArrangement& speaker_arrangement) { - message << ", <" << speaker_arrangement.speakers.size() - << " input_speakers>"; - }}, - *value_payload); - } - - std::visit( - overload{ - [&](const std::nullptr_t&) {}, - [&](const std::string& s) { - if (s.size() < 32) { - message << ", \"" << s << "\""; - } else { - // Long strings contain binary data that we probably - // don't want to print - message << ", <" << s.size() << " bytes>"; - } - }, - [&](const ChunkData& chunk) { - message << ", <" << chunk.buffer.size() << " byte chunk>"; - }, - [&](const AEffect&) { message << ", "; }, - [&](const DynamicSpeakerArrangement& speaker_arrangement) { - message << ", <" << speaker_arrangement.speakers.size() - << " output_speakers>"; - }, - [&](const VstIOProperties&) { message << ", "; }, - [&](const VstMidiKeyName&) { message << ", "; }, - [&](const VstParameterProperties& props) { - message << ", "; - }, - [&](const VstRect& rect) { - message << ", {l: " << rect.left << ", t: " << rect.top - << ", r: " << rect.right << ", b: " << rect.bottom - << "}"; - }, - [&](const VstTimeInfo& info) { - message << ", <" - << "quarter_notes = " << info.ppqPos - << ", samples = " << info.samplePos << ">"; - }}, - payload); - - log(message.str()); - } -} - -bool Logger::should_filter_event(bool is_dispatch, int opcode) const { - if (verbosity >= Verbosity::all_events) { - return false; - } - - // Filter out log messages related to these events by default since they are - // called tens of times per second - // TODO: Figure out what opcode 52 is - if ((is_dispatch && - (opcode == effEditIdle || opcode == 52 || opcode == effIdle)) || - (!is_dispatch && (opcode == audioMasterGetTime || - opcode == audioMasterGetCurrentProcessLevel))) { - return true; - } - - return false; -} +Vst2Logger::Vst2Logger(Logger& generic_logger) : logger(generic_logger) {} std::optional opcode_to_string(bool is_dispatch, int opcode) { if (is_dispatch) { @@ -621,3 +316,217 @@ std::optional opcode_to_string(bool is_dispatch, int opcode) { } } } + +void Vst2Logger::log_get_parameter(int index) { + if (BOOST_UNLIKELY(logger.verbosity >= Logger::Verbosity::most_events)) { + std::ostringstream message; + message << ">> getParameter() " << index; + + log(message.str()); + } +} + +void Vst2Logger::log_get_parameter_response(float value) { + if (BOOST_UNLIKELY(logger.verbosity >= Logger::Verbosity::most_events)) { + std::ostringstream message; + message << " getParameter() :: " << value; + + log(message.str()); + } +} + +void Vst2Logger::log_set_parameter(int index, float value) { + if (BOOST_UNLIKELY(logger.verbosity >= Logger::Verbosity::most_events)) { + std::ostringstream message; + message << ">> setParameter() " << index << " = " << value; + + log(message.str()); + } +} + +void Vst2Logger::log_set_parameter_response() { + if (BOOST_UNLIKELY(logger.verbosity >= Logger::Verbosity::most_events)) { + log(" setParameter() :: OK"); + } +} + +void Vst2Logger::log_event(bool is_dispatch, + int opcode, + int index, + intptr_t value, + const EventPayload& payload, + float option, + const std::optional& value_payload) { + if (BOOST_UNLIKELY(logger.verbosity >= Logger::Verbosity::most_events)) { + if (should_filter_event(is_dispatch, opcode)) { + return; + } + + std::ostringstream message; + if (is_dispatch) { + message << ">> dispatch() "; + } else { + message << ">> audioMasterCallback() "; + } + + const auto opcode_name = opcode_to_string(is_dispatch, opcode); + if (opcode_name) { + message << *opcode_name; + } else { + message << ""; + } + + message << "(index = " << index << ", value = " << value + << ", option = " << option << ", data = "; + + // Only used during `effSetSpeakerArrangement` and + // `effGetSpeakerArrangement` + if (value_payload) { + std::visit( + overload{ + [&](auto) {}, + [&](const DynamicSpeakerArrangement& speaker_arrangement) { + message << "<" << speaker_arrangement.speakers.size() + << " input_speakers>, "; + }}, + *value_payload); + } + + std::visit( + overload{ + [&](const std::nullptr_t&) { message << ""; }, + [&](const std::string& s) { + if (s.size() < 32) { + message << "\"" << s << "\""; + } else { + // Long strings contain binary data that we probably + // don't want to print + message << "<" << s.size() << " bytes>"; + } + }, + [&](const ChunkData& chunk) { + message << "<" << chunk.buffer.size() << " byte chunk>"; + }, + [&](const native_size_t& window_id) { + message << ""; + }, + [&](const AEffect&) { message << ""; }, + [&](const DynamicVstEvents& events) { + message << "<" << events.events.size() << " midi_events>"; + }, + [&](const DynamicSpeakerArrangement& speaker_arrangement) { + message << "<" << speaker_arrangement.speakers.size() + << " output_speakers>"; + }, + [&](const VstIOProperties&) { message << ""; }, + [&](const VstMidiKeyName&) { message << ""; }, + [&](const VstParameterProperties&) { + message << ""; + }, + [&](const WantsAEffectUpdate&) { message << ""; }, + [&](const WantsChunkBuffer&) { + message << ""; + }, + [&](const WantsVstRect&) { message << ""; }, + [&](const WantsVstTimeInfo&) { message << ""; }, + [&](const WantsString&) { message << ""; }}, + payload); + + message << ")"; + + log(message.str()); + } +} + +void Vst2Logger::log_event_response( + bool is_dispatch, + int opcode, + intptr_t return_value, + const EventResultPayload& payload, + const std::optional& value_payload) { + if (BOOST_UNLIKELY(logger.verbosity >= Logger::Verbosity::most_events)) { + if (should_filter_event(is_dispatch, opcode)) { + return; + } + + std::ostringstream message; + if (is_dispatch) { + message << " dispatch() :: "; + } else { + message << " audioMasterCallback() :: "; + } + + message << return_value; + + // Only used during `effSetSpeakerArrangement` and + // `effGetSpeakerArrangement` + if (value_payload) { + std::visit( + overload{ + [&](auto) {}, + [&](const DynamicSpeakerArrangement& speaker_arrangement) { + message << ", <" << speaker_arrangement.speakers.size() + << " input_speakers>"; + }}, + *value_payload); + } + + std::visit( + overload{ + [&](const std::nullptr_t&) {}, + [&](const std::string& s) { + if (s.size() < 32) { + message << ", \"" << s << "\""; + } else { + // Long strings contain binary data that we probably + // don't want to print + message << ", <" << s.size() << " bytes>"; + } + }, + [&](const ChunkData& chunk) { + message << "<" << chunk.buffer.size() << " byte chunk>"; + }, + [&](const AEffect&) { message << ", "; }, + [&](const DynamicSpeakerArrangement& speaker_arrangement) { + message << ", <" << speaker_arrangement.speakers.size() + << " output_speakers>"; + }, + [&](const VstIOProperties&) { message << ", "; }, + [&](const VstMidiKeyName&) { message << ", "; }, + [&](const VstParameterProperties& props) { + message << ", "; + }, + [&](const VstRect& rect) { + message << ", {l: " << rect.left << ", t: " << rect.top + << ", r: " << rect.right << ", b: " << rect.bottom + << "}"; + }, + [&](const VstTimeInfo& info) { + message << ", <" + << "quarter_notes = " << info.ppqPos + << ", samples = " << info.samplePos << ">"; + }}, + payload); + + log(message.str()); + } +} + +bool Vst2Logger::should_filter_event(bool is_dispatch, int opcode) const { + if (logger.verbosity >= Logger::Verbosity::all_events) { + return false; + } + + // Filter out log messages related to these events by default since they are + // called tens of times per second + // TODO: Figure out what opcode 52 is + if ((is_dispatch && + (opcode == effEditIdle || opcode == 52 || opcode == effIdle)) || + (!is_dispatch && (opcode == audioMasterGetTime || + opcode == audioMasterGetCurrentProcessLevel))) { + return true; + } + + return false; +} diff --git a/src/common/logging/vst2.h b/src/common/logging/vst2.h new file mode 100644 index 00000000..75af6412 --- /dev/null +++ b/src/common/logging/vst2.h @@ -0,0 +1,90 @@ +// 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 . + +#pragma once + +#include "../serialization/vst2.h" +#include "common.h" + +/** + * Convert an event opcode to a human readable string for debugging purposes. + * See `src/include/vestige/aeffectx.h` for a complete list of these opcodes. + * + * @param is_dispatch Whether to use opcodes for the `dispatch` function. Will + * use the names from the host callback function if set to false. + * @param opcode The opcode of the event. + * + * @return Either the name from `aeffectx.h`, or a nullopt if it was not listed + * there. + */ +std::optional opcode_to_string(bool is_dispatch, int opcode); + +/** + * Wraps around `Logger` to provide VST2 specific logging functionality for + * debugging plugins. This way we can have all the complex initialisation be + * performed in one place. + */ +class Vst2Logger { + public: + Vst2Logger(Logger& generic_logger); + + /** + * @see Logger::log + */ + inline void log(const std::string& message) { logger.log(message); } + + /** + * @see Logger::log_trace + */ + inline void log_trace(const std::string& message) { + logger.log_trace(message); + } + + // The following functions are for logging specific events, they are only + // enabled for verbosity levels higher than 1 (i.e. `Verbosity::events`) + void log_get_parameter(int index); + void log_get_parameter_response(float vlaue); + void log_set_parameter(int index, float value); + void log_set_parameter_response(); + // If `is_dispatch` is `true`, then use opcode names from the plugin's + // dispatch function. Otherwise use names for the host callback function + // opcodes. + void log_event(bool is_dispatch, + int opcode, + int index, + intptr_t value, + const EventPayload& payload, + float option, + const std::optional& value_payload); + void log_event_response( + bool is_dispatch, + int opcode, + intptr_t return_value, + const EventResultPayload& payload, + const std::optional& value_payload); + + /** + * The underlying logger instance we're wrapping. + */ + Logger& logger; + + private: + /** + * Determine whether an event should be filtered based on the current + * verbosity level. + */ + bool should_filter_event(bool is_dispatch, int opcode) const; +}; diff --git a/src/common/logging/vst3.cpp b/src/common/logging/vst3.cpp new file mode 100644 index 00000000..70388205 --- /dev/null +++ b/src/common/logging/vst3.cpp @@ -0,0 +1,1220 @@ +// 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 . + +#include "vst3.h" + +#include + +#include + +#include "src/common/serialization/vst3.h" + +Vst3Logger::Vst3Logger(Logger& generic_logger) : logger(generic_logger) {} + +void Vst3Logger::log_unknown_interface( + const std::string& where, + const std::optional& uid) { + if (BOOST_UNLIKELY(logger.verbosity >= Logger::Verbosity::most_events)) { + std::string uid_string = uid ? format_uid(*uid) : ""; + + std::ostringstream message; + message << "[unknown interface] " << where << ": " << uid_string; + + log(message.str()); + } +} + +bool Vst3Logger::log_request(bool is_host_vst, + const Vst3PlugViewProxy::Destruct& request) { + return log_request_base(is_host_vst, [&](auto& message) { + // We don't know what class this instance was originally instantiated + // as, but it also doesn't really matter + message << request.owner_instance_id << ": IPlugView::~IPlugView()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const Vst3PluginProxy::Construct& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << "IPluginFactory::createInstance(cid = " + << format_uid(Steinberg::FUID::fromTUID(request.cid.data())) + << ", _iid = "; + switch (request.requested_interface) { + case Vst3PluginProxy::Construct::Interface::IComponent: + message << "IComponent::iid"; + break; + case Vst3PluginProxy::Construct::Interface::IEditController: + message << "IEditController::iid"; + break; + } + message << ", &obj)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const Vst3PluginProxy::Destruct& request) { + return log_request_base(is_host_vst, [&](auto& message) { + // We don't know what class this instance was originally instantiated + // as, but it also doesn't really matter + message << request.instance_id << ": FUnknown::~FUnknown()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const Vst3PluginProxy::SetState& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": {IComponent,IEditController}::setState(state = " + ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const Vst3PluginProxy::GetState& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message + << request.instance_id + << ": {IComponent,IEditController}::getState(state = )"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaConnectionPoint::Connect& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IConnectionPoint::connect(other = "; + std::visit( + overload{[&](const native_size_t& other_instance_id) { + message << ""; + }, + [&](const Vst3ConnectionPointProxy::ConstructArgs&) { + message << ""; + }}, + request.other); + message << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaConnectionPoint::Disconnect& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IConnectionPoint::disconnect(other = "; + if (request.other_instance_id) { + message << ""; + } else { + message << ""; + } + message << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaConnectionPoint::Notify& request) { + return log_request_base(is_host_vst, [&](auto& message) { + // We can safely print the pointer as long we don't dereference it + message << request.instance_id + << ": IConnectionPoint::notify(message = (request.message_ptr).getMessageID()) { + message << " with ID = \"" << id << "\""; + } else { + message << " without an ID"; + } + message << ">)"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaEditController::SetComponentState& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController::setComponentState(state = )"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaEditController::GetParameterCount& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController::getParameterCount()"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaEditController::GetParameterInfo& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController::getParameterInfo(paramIndex = " + << request.param_index << ", &info)"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaEditController::GetParamStringByValue& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController::getParamStringByValue(id = " + << request.id + << ", valueNormalized = " << request.value_normalized + << ", &string)"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaEditController::GetParamValueByString& request) { + return log_request_base(is_host_vst, [&](auto& message) { + std::string param_title = VST3::StringConvert::convert(request.string); + message << request.instance_id + << ": IEditController::getParamValueByString(id = " + << request.id << ", string = " << param_title + << ", &valueNormalized)"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaEditController::NormalizedParamToPlain& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController::normalizedParamToPlain(id = " + << request.id + << ", valueNormalized = " << request.value_normalized << ")"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaEditController::PlainParamToNormalized& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController::plainParamToNormalized(id = " + << request.id << ", plainValue = " << request.plain_value + << ")"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaEditController::GetParamNormalized& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController::getParamNormalized(id = " << request.id + << ")"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaEditController::SetParamNormalized& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController::setParamNormalized(id = " << request.id + << ", value = " << request.value << ")"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaEditController::SetComponentHandler& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController::setComponentHandler(handler = " + ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaEditController::CreateView& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController::createView(name = \"" << request.name + << "\")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaEditController2::SetKnobMode& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController2::setKnobMode(mode = " << request.mode + << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaEditController2::OpenHelp& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController2::openHelp(onlyCheck = " + << (request.only_check ? "true" : "false") << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaEditController2::OpenAboutBox& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IEditController2::openAboutBox(onlyCheck = " + << (request.only_check ? "true" : "false") << ")"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaPlugView::IsPlatformTypeSupported& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IPlugView::isPLatformTypeSupported(type = \"" + << request.type; + if (request.type == Steinberg::kPlatformTypeX11EmbedWindowID) { + message << "\" (will be translated to \"" + << Steinberg::kPlatformTypeHWND << "\")"; + } + message << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::Attached& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IPlugView::attached(parent = " << request.parent + << ", type = \"" << request.type; + if (request.type == Steinberg::kPlatformTypeX11EmbedWindowID) { + message << "\" (will be translated to \"" + << Steinberg::kPlatformTypeHWND << "\")"; + } + message << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::Removed& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id << ": IPlugView::removed()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::OnWheel& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IPlugView::onWheel(distance = " << request.distance + << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::OnKeyDown& request) { + return log_request_base(is_host_vst, [&](auto& message) { + // This static cast is technically not correct of course but it's + // UTF-16, so everything's allowed + message << request.owner_instance_id << ": IPlugView::onKeyDown(key = " + << static_cast(request.key) + << ", keyCode = " << request.key_code + << ", modifiers = " << request.modifiers << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::OnKeyUp& request) { + return log_request_base(is_host_vst, [&](auto& message) { + // This static cast is technically not correct of course but it's + // UTF-16, so everything's allowed + message << request.owner_instance_id << ": IPlugView::onKeyUp(key = " + << static_cast(request.key) + << ", keyCode = " << request.key_code + << ", modifiers = " << request.modifiers << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::GetSize& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id << ": IPlugView::getSize(size*)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::OnSize& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IPlugView::onSize(newSize = )"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::OnFocus& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id << ": IPlugView::onFucus(state = " + << (request.state ? "true" : "false") << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::SetFrame& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IPlugView::setFrame(frame = )"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::CanResize& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id << ": IPlugView::canResize()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugView::CheckSizeConstraint& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IPlugView::checkSizeConstraint(rect = " + ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPluginBase::Initialize& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IPluginBase::initialize(context = )"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPluginBase::Terminate& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id << ": IPluginBase::terminate()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPluginFactory::Construct&) { + return log_request_base( + is_host_vst, [&](auto& message) { message << "GetPluginFactory()"; }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPluginFactory::SetHostContext&) { + return log_request_base(is_host_vst, [&](auto& message) { + message << "IPluginFactory3::setHostContext(context = )"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaProgramListData::ProgramDataSupported&) { + return log_request_base(is_host_vst, [&](auto& message) { + message << "IProgramListData::programDataSupported()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaProgramListData::GetProgramData& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << "IProgramListData::getProgramData(listId = " + << request.list_id + << ", programIndex = " << request.program_index << ", &data)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaProgramListData::SetProgramData& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << "IProgramListData::setProgramData(listId = " + << request.list_id + << ", programIndex = " << request.program_index + << ", data = )"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitData::UnitDataSupported&) { + return log_request_base(is_host_vst, [&](auto& message) { + message << "IUnitData::unitDataSupported()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitData::GetUnitData& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << "IUnitData::getUnitData(listId = " << request.unit_id + << ", &data)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitData::SetUnitData& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << "IUnitData::setUnitData(listId = " << request.unit_id + << ", data = )"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::GetUnitCount& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id << ": IUnitInfo::getUnitCount()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::GetUnitInfo& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IUnitInfo::getUnitInfo(unitIndex = " << request.unit_index + << ", &info)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::GetProgramListCount& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id << ": IUnitInfo::getProgramListCount()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::GetProgramListInfo& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IUnitInfo::getProgramListInfo(listIndex = " + << request.list_index << ", &info)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::GetProgramName& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IUnitInfo::getProgramName(listId = " << request.list_id + << ", programIndex = " << request.program_index << ", &name)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::GetProgramInfo& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IUnitInfo::getProgramInfo(listId = " << request.list_id + << ", programIndex = " << request.program_index + << ", attributeId = " << request.attribute_id + << ", &attributeValue)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::HasProgramPitchNames& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IUnitInfo::hasProgramPitchNames(listId = " + << request.list_id + << ", programIndex = " << request.program_index << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::GetProgramPitchName& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IUnitInfo::getProgramPitchName(listId = " + << request.list_id + << ", programIndex = " << request.program_index + << ", midiPitch = " << request.midi_pitch << ", &name)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::GetSelectedUnit& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id << ": IUnitInfo::getSelectedUnit()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::SelectUnit& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IUnitInfo::selectUnit(unitId = " << request.unit_id + << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::GetUnitByBus& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IUnitInfo::getUnitByBus(type = " << request.type + << ", dir = " << request.dir + << ", busIndex = " << request.bus_index + << ", channel = " << request.channel << ", &unitId)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaUnitInfo::SetUnitProgramData& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IUnitInfo::setUnitProgramData(listOrUnitId = " + << request.list_or_unit_id + << ", programIndex = " << request.program_index + << ", data = )"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaAudioProcessor::SetBusArrangements& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IAudioProcessor::setBusArrangements(inputs = " + "["; + + for (bool first = true; const auto& arrangement : request.inputs) { + if (!first) { + message << ", "; + } + message << "SpeakerArrangement: 0b" + << std::bitset( + arrangement); + first = false; + } + + message << "], numIns = " << request.num_ins << ", outputs = ["; + + for (bool first = true; const auto& arrangement : request.outputs) { + if (!first) { + message << ", "; + } + message << "SpeakerArrangement: 0b" + << std::bitset( + arrangement); + first = false; + } + + message << "], numOuts = " << request.num_outs << ")"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaAudioProcessor::GetBusArrangement& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IAudioProcessor::getBusArrangement(dir = " << request.dir + << ", index = " << request.index << ", &arr)"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaAudioProcessor::CanProcessSampleSize& request) { + return log_request_base( + is_host_vst, Logger::Verbosity::all_events, [&](auto& message) { + message + << request.instance_id + << ": IAudioProcessor::canProcessSampleSize(symbolicSampleSize " + "= " + << request.symbolic_sample_size << ")"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaAudioProcessor::GetLatencySamples& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IAudioProcessor::getLatencySamples()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaAudioProcessor::SetupProcessing& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IAudioProcessor::setupProcessing(setup = " + ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaAudioProcessor::SetProcessing& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IAudioProcessor::setProcessing(state = " + << (request.state ? "true" : "false") << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaAudioProcessor::Process& request) { + return log_request_base( + is_host_vst, Logger::Verbosity::all_events, [&](auto& message) { + // This is incredibly verbose, but if you're really a plugin that + // handles processing in a weird way you're going to need all of + // this + + std::ostringstream num_input_channels; + num_input_channels << "["; + for (bool is_first = true; + const auto& buffers : request.data.inputs) { + num_input_channels << (is_first ? "" : ", ") + << buffers.num_channels(); + is_first = false; + } + num_input_channels << "]"; + + std::ostringstream num_output_channels; + num_output_channels << "["; + for (bool is_first = true; + const auto& num_channels : request.data.outputs_num_channels) { + num_output_channels << (is_first ? "" : ", ") << num_channels; + is_first = false; + } + num_output_channels << "]"; + + message << request.instance_id + << ": IAudioProcessor::process(data = , output_parameter_changes = " + << (request.data.output_parameter_changes_supported + ? "" + : "nullptr") + << ", input_events = "; + if (request.data.input_events) { + message << "num_events() + << " events>"; + } else { + message << ""; + } + message << ", output_events = " + << (request.data.output_events_supported ? "" + : "") + << ", process_context = " + << (request.data.process_context ? "" + : "") + << ", process_mode = " << request.data.process_mode + << ", symbolic_sample_size = " + << request.data.symbolic_sample_size << ">)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaAudioProcessor::GetTailSamples& request) { + return log_request_base( + is_host_vst, Logger::Verbosity::all_events, [&](auto& message) { + message << request.instance_id + << ": IAudioProcessor::getTailSamples()"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaComponent::GetControllerClassId& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IComponent::getControllerClassId(&classId)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaComponent::SetIoMode& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IComponent::setIoMode(mode = " << request.mode << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaComponent::GetBusCount& request) { + // JUCE-based hosts will call this every processing cycle, for some reason + // (it shouldn't be allwoed to change during processing, right?) + return log_request_base( + is_host_vst, Logger::Verbosity::all_events, [&](auto& message) { + message << request.instance_id + << ": IComponent::getBusCount(type = " << request.type + << ", dir = " << request.dir << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaComponent::GetBusInfo& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IComponent::getBusInfo(type = " << request.type + << ", dir = " << request.dir << ", index = " << request.index + << ", &bus)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaComponent::GetRoutingInfo& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message + << request.instance_id + << ": IComponent::getRoutingInfo(inInfo = , outInfo = )"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaComponent::ActivateBus& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id + << ": IComponent::activateBus(type = " << request.type + << ", dir = " << request.dir << ", index = " << request.index + << ", state = " << (request.state ? "true" : "false") << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaComponent::SetActive& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.instance_id << ": IComponent::setActive(state = " + << (request.state ? "true" : "false") << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, const WantsConfiguration&) { + return log_request_base(is_host_vst, [&](auto& message) { + message << "Requesting "; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaComponentHandler::BeginEdit& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IComponentHandler::beginEdit(id = " << request.id << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaComponentHandler::PerformEdit& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IComponentHandler::performEdit(id = " << request.id + << ", valueNormalized = " << request.value_normalized << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaComponentHandler::EndEdit& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IComponentHandler::endEdit(id = " << request.id << ")"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaComponentHandler::RestartComponent& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IComponentHandler::restartComponent(flags = " + << request.flags << ")"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaHostApplication::GetName& request) { + return log_request_base(is_host_vst, [&](auto& message) { + // This can be called either from a plugin object or from the plugin's + // plugin factory + if (request.owner_instance_id) { + message << *request.owner_instance_id << ": "; + } + + message << "IHostApplication::getName(&name)"; + }); +} + +bool Vst3Logger::log_request(bool is_host_vst, + const YaPlugFrame::ResizeView& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IPlugFrame::resizeView(view = , newSize = " + ")"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaUnitHandler::NotifyUnitSelection& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IUnitHandler::notifyUnitSelection(unitId = " + << request.unit_id << ")"; + }); +} + +bool Vst3Logger::log_request( + bool is_host_vst, + const YaUnitHandler::NotifyProgramListChange& request) { + return log_request_base(is_host_vst, [&](auto& message) { + message << request.owner_instance_id + << ": IUnitHandler::notifyProgramListChange(listId = " + << request.list_id + << ", programIndex = " << request.program_index << ")"; + }); +} + +void Vst3Logger::log_response(bool is_host_vst, const Ack&) { + log_response_base(is_host_vst, [&](auto& message) { message << "ACK"; }); +} + +void Vst3Logger::log_response(bool is_host_vst, + const std::variant& result) { + log_response_base(is_host_vst, [&](auto& message) { + std::visit(overload{[&](const Vst3PluginProxy::ConstructArgs& args) { + message << ""; + }, + [&](const UniversalTResult& code) { + message << code.string(); + }}, + result); + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const Vst3PluginProxy::GetStateResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", "; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaEditController::GetParameterInfoResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + std::string param_title = + VST3::StringConvert::convert(response.updated_info.title); + message << ", "; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaEditController::GetParamStringByValueResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + std::string value = VST3::StringConvert::convert(response.string); + message << ", \"" << value << "\""; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaEditController::GetParamValueByStringResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", " << response.value_normalized; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaEditController::CreateViewResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + if (response.plug_view_args) { + message << ""; + } else { + message << ""; + } + }); +} + +void Vst3Logger::log_response(bool is_host_vst, + const YaPlugView::GetSizeResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", "; + } + }); +} + +void Vst3Logger::log_response(bool is_host_vst, + const YaPluginFactory::ConstructArgs& args) { + log_response_base(is_host_vst, [&](auto& message) { + message << ""; + }); +} + +void Vst3Logger::log_response(bool is_host_vst, const Configuration&) { + log_response_base(is_host_vst, + [&](auto& message) { message << ""; }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaProgramListData::GetProgramDataResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", "; + } + }); +} + +void Vst3Logger::log_response(bool is_host_vst, + const YaUnitData::GetUnitDataResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", "; + } + }); +} + +void Vst3Logger::log_response(bool is_host_vst, + const YaUnitInfo::GetUnitInfoResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", "; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaUnitInfo::GetProgramListInfoResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", "; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaUnitInfo::GetProgramNameResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", \"" << VST3::StringConvert::convert(response.name) + << "\""; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaUnitInfo::GetProgramInfoResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", \"" + << VST3::StringConvert::convert(response.attribute_value) + << "\""; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaUnitInfo::GetProgramPitchNameResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", \"" << VST3::StringConvert::convert(response.name) + << "\""; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaUnitInfo::GetUnitByBusResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", unit #" << response.unit_id; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaAudioProcessor::GetBusArrangementResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", ( + response.updated_arr) + << ">"; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaAudioProcessor::ProcessResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + + // This is incredibly verbose, but if you're really a plugin that + // handles processing in a weird way you're going to need all of this + + std::ostringstream num_output_channels; + num_output_channels << "["; + for (bool is_first = true; + const auto& buffers : response.output_data.outputs) { + num_output_channels << (is_first ? "" : ", ") + << buffers.num_channels(); + is_first = false; + } + num_output_channels << "]"; + + message << ", "; + + if (response.output_data.output_parameter_changes) { + message << ", num_parameters() + << " parameters>"; + } else { + message << ", host does not support parameter outputs"; + } + + if (response.output_data.output_events) { + message << ", num_events() + << " events>"; + } else { + message << ", host does not support event outputs"; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaComponent::GetControllerClassIdResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", " + << format_uid(Steinberg::FUID::fromTUID( + response.editor_cid.data())); + } + }); +} + +void Vst3Logger::log_response(bool is_host_vst, + const YaComponent::GetBusInfoResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", "; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaComponent::GetRoutingInfoResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + message << ", "; + } + }); +} + +void Vst3Logger::log_response( + bool is_host_vst, + const YaHostApplication::GetNameResponse& response) { + log_response_base(is_host_vst, [&](auto& message) { + message << response.result.string(); + if (response.result == Steinberg::kResultOk) { + std::string value = VST3::StringConvert::convert(response.name); + message << ", \"" << value << "\""; + } + }); +} diff --git a/src/common/logging/vst3.h b/src/common/logging/vst3.h new file mode 100644 index 00000000..7cf87565 --- /dev/null +++ b/src/common/logging/vst3.h @@ -0,0 +1,276 @@ +// 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 . + +#pragma once + +#include + +#include "../serialization/vst3.h" +#include "common.h" + +/** + * Wraps around `Logger` to provide VST3 specific logging functionality for + * debugging plugins. This way we can have all the complex initialisation be + * performed in one place. + */ +class Vst3Logger { + public: + Vst3Logger(Logger& generic_logger); + + /** + * @see Logger::log + */ + inline void log(const std::string& message) { logger.log(message); } + + /** + * @see Logger::log_trace + */ + inline void log_trace(const std::string& message) { + logger.log_trace(message); + } + + /** + * Log about encountering an unknown interface. The location and the UID + * will be printed when the verbosity level is set to `most_events` or + * higher. In case we could not get a FUID (because of null pointers, for + * instance), `std::nullopt` should be passed. + */ + void log_unknown_interface(const std::string& where, + const std::optional& uid); + + // For every object we send using `Vst3MessageHandler` we have overloads + // that print information about the request and the response. The boolean + // flag here indicates whether the request was initiated on the host side + // (what we'll call a control message). + // `log_response()` should only be called if the corresponding + // `log_request()` call returned `true`. This way we can filter out the + // log message for the response together with the request. + + bool log_request(bool is_host_vst, const Vst3PlugViewProxy::Destruct&); + bool log_request(bool is_host_vst, const Vst3PluginProxy::Construct&); + bool log_request(bool is_host_vst, const Vst3PluginProxy::Destruct&); + bool log_request(bool is_host_vst, const Vst3PluginProxy::SetState&); + bool log_request(bool is_host_vst, const Vst3PluginProxy::GetState&); + bool log_request(bool is_host_vst, const YaConnectionPoint::Connect&); + bool log_request(bool is_host_vst, const YaConnectionPoint::Disconnect&); + bool log_request(bool is_host_vst, const YaConnectionPoint::Notify&); + bool log_request(bool is_host_vst, + const YaEditController::SetComponentState&); + bool log_request(bool is_host_vst, + const YaEditController::GetParameterCount&); + bool log_request(bool is_host_vst, + const YaEditController::GetParameterInfo&); + bool log_request(bool is_host_vst, + const YaEditController::GetParamStringByValue&); + bool log_request(bool is_host_vst, + const YaEditController::GetParamValueByString&); + bool log_request(bool is_host_vst, + const YaEditController::NormalizedParamToPlain&); + bool log_request(bool is_host_vst, + const YaEditController::PlainParamToNormalized&); + bool log_request(bool is_host_vst, + const YaEditController::GetParamNormalized&); + bool log_request(bool is_host_vst, + const YaEditController::SetParamNormalized&); + bool log_request(bool is_host_vst, + const YaEditController::SetComponentHandler&); + bool log_request(bool is_host_vst, const YaEditController::CreateView&); + bool log_request(bool is_host_vst, const YaEditController2::SetKnobMode&); + bool log_request(bool is_host_vst, const YaEditController2::OpenHelp&); + bool log_request(bool is_host_vst, const YaEditController2::OpenAboutBox&); + bool log_request(bool is_host_vst, + const YaPlugView::IsPlatformTypeSupported&); + bool log_request(bool is_host_vst, const YaPlugView::Attached&); + bool log_request(bool is_host_vst, const YaPlugView::Removed&); + bool log_request(bool is_host_vst, const YaPlugView::OnWheel&); + bool log_request(bool is_host_vst, const YaPlugView::OnKeyDown&); + bool log_request(bool is_host_vst, const YaPlugView::OnKeyUp&); + bool log_request(bool is_host_vst, const YaPlugView::GetSize&); + bool log_request(bool is_host_vst, const YaPlugView::OnSize&); + bool log_request(bool is_host_vst, const YaPlugView::OnFocus&); + bool log_request(bool is_host_vst, const YaPlugView::SetFrame&); + bool log_request(bool is_host_vst, const YaPlugView::CanResize&); + bool log_request(bool is_host_vst, const YaPlugView::CheckSizeConstraint&); + bool log_request(bool is_host_vst, const YaPluginBase::Initialize&); + bool log_request(bool is_host_vst, const YaPluginBase::Terminate&); + bool log_request(bool is_host_vst, const YaPluginFactory::Construct&); + bool log_request(bool is_host_vst, const YaPluginFactory::SetHostContext&); + bool log_request(bool is_host_vst, + const YaProgramListData::ProgramDataSupported&); + bool log_request(bool is_host_vst, + const YaProgramListData::GetProgramData&); + bool log_request(bool is_host_vst, + const YaProgramListData::SetProgramData&); + bool log_request(bool is_host_vst, const YaUnitData::UnitDataSupported&); + bool log_request(bool is_host_vst, const YaUnitData::GetUnitData&); + bool log_request(bool is_host_vst, const YaUnitData::SetUnitData&); + bool log_request(bool is_host_vst, const YaUnitInfo::GetUnitCount&); + bool log_request(bool is_host_vst, const YaUnitInfo::GetUnitInfo&); + bool log_request(bool is_host_vst, const YaUnitInfo::GetProgramListCount&); + bool log_request(bool is_host_vst, const YaUnitInfo::GetProgramListInfo&); + bool log_request(bool is_host_vst, const YaUnitInfo::GetProgramName&); + bool log_request(bool is_host_vst, const YaUnitInfo::GetProgramInfo&); + bool log_request(bool is_host_vst, const YaUnitInfo::HasProgramPitchNames&); + bool log_request(bool is_host_vst, const YaUnitInfo::GetProgramPitchName&); + bool log_request(bool is_host_vst, const YaUnitInfo::GetSelectedUnit&); + bool log_request(bool is_host_vst, const YaUnitInfo::SelectUnit&); + bool log_request(bool is_host_vst, const YaUnitInfo::GetUnitByBus&); + bool log_request(bool is_host_vst, const YaUnitInfo::SetUnitProgramData&); + + bool log_request(bool is_host_vst, + const YaAudioProcessor::SetBusArrangements&); + bool log_request(bool is_host_vst, + const YaAudioProcessor::GetBusArrangement&); + bool log_request(bool is_host_vst, + const YaAudioProcessor::CanProcessSampleSize&); + bool log_request(bool is_host_vst, + const YaAudioProcessor::GetLatencySamples&); + bool log_request(bool is_host_vst, + const YaAudioProcessor::SetupProcessing&); + bool log_request(bool is_host_vst, const YaAudioProcessor::SetProcessing&); + bool log_request(bool is_host_vst, const YaAudioProcessor::Process&); + bool log_request(bool is_host_vst, const YaAudioProcessor::GetTailSamples&); + bool log_request(bool is_host_vst, + const YaComponent::GetControllerClassId&); + bool log_request(bool is_host_vst, const YaComponent::SetIoMode&); + bool log_request(bool is_host_vst, const YaComponent::GetBusCount&); + bool log_request(bool is_host_vst, const YaComponent::GetBusInfo&); + bool log_request(bool is_host_vst, const YaComponent::GetRoutingInfo&); + bool log_request(bool is_host_vst, const YaComponent::ActivateBus&); + bool log_request(bool is_host_vst, const YaComponent::SetActive&); + + bool log_request(bool is_host_vst, const WantsConfiguration&); + bool log_request(bool is_host_vst, const YaComponentHandler::BeginEdit&); + bool log_request(bool is_host_vst, const YaComponentHandler::PerformEdit&); + bool log_request(bool is_host_vst, const YaComponentHandler::EndEdit&); + bool log_request(bool is_host_vst, + const YaComponentHandler::RestartComponent&); + bool log_request(bool is_host_vst, const YaHostApplication::GetName&); + bool log_request(bool is_host_vst, const YaPlugFrame::ResizeView&); + bool log_request(bool is_host_vst, + const YaUnitHandler::NotifyUnitSelection&); + bool log_request(bool is_host_vst, + const YaUnitHandler::NotifyProgramListChange&); + + void log_response(bool is_host_vst, const Ack&); + void log_response( + bool is_host_vst, + const std::variant&); + void log_response(bool is_host_vst, + const Vst3PluginProxy::GetStateResponse&); + void log_response(bool is_host_vst, + const YaEditController::GetParameterInfoResponse&); + void log_response(bool is_host_vst, + const YaEditController::GetParamStringByValueResponse&); + void log_response(bool is_host_vst, + const YaEditController::GetParamValueByStringResponse&); + void log_response(bool is_host_vst, + const YaEditController::CreateViewResponse&); + void log_response(bool is_host_vst, const YaPlugView::GetSizeResponse&); + void log_response(bool is_host_vst, const YaPluginFactory::ConstructArgs&); + void log_response(bool is_host_vst, const Configuration&); + void log_response(bool is_host_vst, + const YaProgramListData::GetProgramDataResponse&); + void log_response(bool is_host_vst, const YaUnitData::GetUnitDataResponse&); + void log_response(bool is_host_vst, const YaUnitInfo::GetUnitInfoResponse&); + void log_response(bool is_host_vst, + const YaUnitInfo::GetProgramListInfoResponse&); + void log_response(bool is_host_vst, + const YaUnitInfo::GetProgramNameResponse&); + void log_response(bool is_host_vst, + const YaUnitInfo::GetProgramInfoResponse&); + void log_response(bool is_host_vst, + const YaUnitInfo::GetProgramPitchNameResponse&); + void log_response(bool is_host_vst, + const YaUnitInfo::GetUnitByBusResponse&); + + void log_response(bool is_host_vst, + const YaAudioProcessor::GetBusArrangementResponse&); + void log_response(bool is_host_vst, + const YaAudioProcessor::ProcessResponse&); + void log_response(bool is_host_vst, + const YaComponent::GetControllerClassIdResponse&); + void log_response(bool is_host_vst, const YaComponent::GetBusInfoResponse&); + void log_response(bool is_host_vst, + const YaComponent::GetRoutingInfoResponse&); + + void log_response(bool is_host_vst, + const YaHostApplication::GetNameResponse&); + + template + void log_response(bool is_host_vst, const PrimitiveWrapper& value) { + // For logging all primitive return values other than `tresult` + log_response_base(is_host_vst, + [&](auto& message) { message << value; }); + } + + Logger& logger; + + private: + /** + * Log a request with a standard prefix based on the boolean flag we pass to + * every logging function so we don't have to repeat it everywhere. + * + * Returns `true` if the log message was displayed, and the response should + * thus also be logged. + */ + template F> + bool log_request_base(bool is_host_vst, + Logger::Verbosity min_verbosity, + F callback) { + if (BOOST_UNLIKELY(logger.verbosity >= min_verbosity)) { + std::ostringstream message; + if (is_host_vst) { + message << "[host -> vst] >> "; + } else { + message << "[vst -> host] >> "; + } + + callback(message); + log(message.str()); + + return true; + } else { + return false; + } + } + + template F> + bool log_request_base(bool is_host_vst, F callback) { + return log_request_base(is_host_vst, Logger::Verbosity::most_events, + callback); + } + + /** + * Log a response with a standard prefix based on the boolean flag we pass + * to every logging function so we don't have to repeat it everywhere. + * + * This should only be called when the corresonding `log_request()` returned + * `true`. + */ + template F> + void log_response_base(bool is_host_vst, F callback) { + std::ostringstream message; + if (is_host_vst) { + message << "[vst <- host] "; + } else { + message << "[host <- vst] "; + } + + callback(message); + log(message.str()); + } +}; diff --git a/src/common/plugins.cpp b/src/common/plugins.cpp new file mode 100644 index 00000000..abca7b49 --- /dev/null +++ b/src/common/plugins.cpp @@ -0,0 +1,75 @@ +#include "plugins.h" + +#include +#include + +namespace fs = boost::filesystem; + +LibArchitecture find_dll_architecture(const fs::path& plugin_path) { + std::ifstream file(plugin_path, std::ifstream::binary | std::ifstream::in); + + // The linker will place the offset where the PE signature is placed at the + // end of the MS-DOS stub, at offset 0x3c + uint32_t pe_signature_offset; + file.seekg(0x3c); + file.read(reinterpret_cast(&pe_signature_offset), + sizeof(pe_signature_offset)); + + // The PE32 signature will be followed by a magic number that indicates the + // target architecture of the binary + uint32_t pe_signature; + uint16_t machine_type; + file.seekg(pe_signature_offset); + file.read(reinterpret_cast(&pe_signature), sizeof(pe_signature)); + file.read(reinterpret_cast(&machine_type), sizeof(machine_type)); + + constexpr char expected_pe_signature[4] = {'P', 'E', '\0', '\0'}; + if (pe_signature != + *reinterpret_cast(expected_pe_signature)) { + throw std::runtime_error("'" + plugin_path.string() + + "' is not a valid .dll file"); + } + + // These constants are specified in + // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types + switch (machine_type) { + case 0x014c: // IMAGE_FILE_MACHINE_I386 + return LibArchitecture::dll_32; + break; + case 0x8664: // IMAGE_FILE_MACHINE_AMD64 + case 0x0000: // IMAGE_FILE_MACHINE_UNKNOWN + return LibArchitecture::dll_64; + break; + } + + // When compiled without optimizations, GCC 9.3 will warn that the function + // does not return if we put this in a `default:` case instead. + std::ostringstream error_msg; + error_msg << "'" << plugin_path + << "' is neither a x86 nor a x86_64 PE32 file. Actual " + "architecture: 0x" + << std::hex << machine_type; + throw std::runtime_error(error_msg.str()); +} + +PluginType plugin_type_from_string(const std::string& plugin_type) { + if (plugin_type == "VST2") { + return PluginType::vst2; + } else if (plugin_type == "VST3") { + return PluginType::vst3; + } else { + return PluginType::unknown; + } +} + +std::string plugin_type_to_string(const PluginType& plugin_type) { + // We'll capitalize the acronyms because this is also our human readable + // format + if (plugin_type == PluginType::vst2) { + return "VST2"; + } else if (plugin_type == PluginType::vst3) { + return "VST3"; + } else { + return ""; + } +} diff --git a/src/common/plugins.h b/src/common/plugins.h new file mode 100644 index 00000000..86d4c5be --- /dev/null +++ b/src/common/plugins.h @@ -0,0 +1,64 @@ +// 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 . + +#pragma once + +#ifdef __WINE__ +#include "../wine-host/boost-fix.h" +#endif +#include + +// Utilities and tags for plugin types and architectures + +/** + * A tag to differentiate between 32 and 64-bit `.dll` files, used to determine + * which host application to use. + */ +enum class LibArchitecture { dll_32, dll_64 }; + +/** + * A tag to differentiate between different plugin types. + * `plugin_tyep_to_string()` and `plugin_type_from_string()` can be used to + * convert these values to and from strings. The string form is used as a + * command line argument for the individual Wine host applications, and the enum + * form is passed directly in `HostRequest`. + * + * The `unkonwn` tag is not used directly, but in the event that we do call + * `plugin_type_from_string()` with some invalid value we can use it to + * gracefully show an error message without resorting to exceptions. + */ +enum class PluginType { vst2, vst3, unknown }; + +template +void serialize(S& s, PluginType& plugin_type) { + s.value4b(plugin_type); +} + +/** + * Determine the architecture of a `.dll` file based on the file header. + * + * See https://docs.microsoft.com/en-us/windows/win32/debug/pe-format for more + * information on the PE32 format. + * + * @param path The path to the .dll file we're going to check. + * + * @return The detected architecture. + * @throw std::runtime_error If the file is not a .dll file. + */ +LibArchitecture find_dll_architecture(const boost::filesystem::path&); + +PluginType plugin_type_from_string(const std::string& plugin_type); +std::string plugin_type_to_string(const PluginType& plugin_type); diff --git a/src/common/serialization/common.h b/src/common/serialization/common.h new file mode 100644 index 00000000..3eef07e2 --- /dev/null +++ b/src/common/serialization/common.h @@ -0,0 +1,85 @@ +// 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 . + +#pragma once + +#include + +#include +#include +#include + +#include "../plugins.h" + +// The plugin should always be compiled to a 64-bit version, but the host +// application can also be 32-bit to allow using 32-bit legacy Windows VST in a +// modern Linux VST host. Because of this we have to make sure to always use +// 64-bit integers in places where we would otherwise use `size_t` and +// `intptr_t`. Otherwise the binary serialization would break. The 64 <-> 32 bit +// conversion for the 32-bit host application won't cause any issues for us +// since we can't directly pass pointers between the plugin and the host anyway. + +#ifndef __WINE__ +// Sanity check for the plugin, both the 64 and 32 bit hosts should follow these +// conventions +static_assert(std::is_same_v); +static_assert(std::is_same_v); +#endif +using native_size_t = uint64_t; +using native_intptr_t = int64_t; + +/** + * An object containing the startup options for hosting a plugin. These options + * are passed to `yabridge-host.exe` as command line arguments, and they are + * used directly by group host processes. + */ +struct HostRequest { + PluginType plugin_type; + std::string plugin_path; + std::string endpoint_base_dir; + + template + void serialize(S& s) { + s.object(plugin_type); + s.text1b(plugin_path, 4096); + s.text1b(endpoint_base_dir, 4096); + } +}; + +template <> +struct std::hash { + std::size_t operator()(HostRequest const& params) const noexcept { + std::hash hasher{}; + + return hasher(params.plugin_path) ^ + (hasher(params.endpoint_base_dir) << 1); + } +}; + +/** + * The response sent back after the group host process receives a `HostRequest` + * 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 HostResponse { + pid_t pid; + + template + void serialize(S& s) { + s.value4b(pid); + } +}; diff --git a/src/common/serialization.cpp b/src/common/serialization/vst2.cpp similarity index 99% rename from src/common/serialization.cpp rename to src/common/serialization/vst2.cpp index a764a7e7..f0219559 100644 --- a/src/common/serialization.cpp +++ b/src/common/serialization/vst2.cpp @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include "serialization.h" +#include "vst2.h" DynamicVstEvents::DynamicVstEvents(const VstEvents& c_events) : events(c_events.numEvents) { diff --git a/src/common/serialization.h b/src/common/serialization/vst2.h similarity index 88% rename from src/common/serialization.h rename to src/common/serialization/vst2.h index 8f0e9823..e44dfc16 100644 --- a/src/common/serialization.h +++ b/src/common/serialization/vst2.h @@ -16,18 +16,17 @@ #pragma once -#include -#include +#include + #include #include #include -#include #include #include -#include - -#include "vst24.h" +#include "../utils.h" +#include "../vst24.h" +#include "common.h" // These constants are limits used by bitsery @@ -54,38 +53,12 @@ constexpr size_t max_midi_events = max_buffer_size / sizeof(size_t); [[maybe_unused]] constexpr size_t max_string_length = 64; /** - * The size for a buffer in which we're receiving chunks. Allow for up to 50 MB - * chunks. Hopefully no plugin will come anywhere near this limit, but it will - * add up when plugins start to audio samples in their presets. + * The maximum size for the buffer we're receiving chunks in. Allows for up to + * 50 MB chunks. Hopefully no plugin will come anywhere near this limit, but it + * will add up when plugins start to audio include samples in their presets. */ constexpr size_t binary_buffer_size = 50 << 20; -// The plugin should always be compiled to a 64-bit version, but the host -// application can also be 32-bit to allow using 32-bit legacy Windows VST in a -// modern Linux VST host. Because of this we have to make sure to always use -// 64-bit integers in places where we would otherwise use `size_t` and -// `intptr_t`. Otherwise the binary serialization would break. The 64 <-> 32 bit -// conversion for the 32-bit host application won't cause any issues for us -// since we can't directly pass pointers between the plugin and the host anyway. - -#ifndef __WINE__ -// Sanity check for the plugin, both the 64 and 32 bit hosts should follow these -// conventions -static_assert(std::is_same_v); -static_assert(std::is_same_v); -#endif -using native_size_t = uint64_t; -using native_intptr_t = int64_t; - -// The cannonical overloading template for `std::visitor`, not sure why this -// isn't part of the standard library -template -struct overload : Ts... { - using Ts::operator()...; -}; -template -overload(Ts...) -> overload; - /** * Update an `AEffect` object, copying values from `updated_plugin` to `plugin`. * This will copy all flags and regular values, leaving all pointers in `plugin` @@ -458,7 +431,7 @@ struct Event { * - A (short) string. * - Some binary blob stored as a byte vector. During `effGetChunk` this will * contain some chunk data that should be written to - * `PluginBridge::chunk_data`. + * `Vst2PluginBridge::chunk_data`. * - A specific struct in response to an event such as `audioMasterGetTime` or * `audioMasterIOChanged`. * - An X11 window pointer for the editor window. @@ -601,44 +574,3 @@ 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 endpoint_base_dir; - - template - void serialize(S& s) { - s.text1b(plugin_path, 4096); - s.text1b(endpoint_base_dir, 4096); - } -}; - -template <> -struct std::hash { - std::size_t operator()(GroupRequest const& params) const noexcept { - std::hash hasher{}; - - return hasher(params.plugin_path) ^ - (hasher(params.endpoint_base_dir) << 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 - void serialize(S& s) { - s.value4b(pid); - } -}; diff --git a/src/common/serialization/vst3.h b/src/common/serialization/vst3.h new file mode 100644 index 00000000..5d925107 --- /dev/null +++ b/src/common/serialization/vst3.h @@ -0,0 +1,183 @@ +// 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 . + +#pragma once + +#include + +#include + +#include "../configuration.h" +#include "../utils.h" +#include "common.h" +#include "vst3/component-handler-proxy.h" +#include "vst3/connection-point-proxy.h" +#include "vst3/host-context-proxy.h" +#include "vst3/plug-frame-proxy.h" +#include "vst3/plug-view-proxy.h" +#include "vst3/plugin-factory.h" +#include "vst3/plugin-proxy.h" + +// Event handling for our VST3 plugins works slightly different from how we +// handle VST2 plugins. VST3 does not have a centralized event dispatching +// interface like VST2 does, and it uses a bunch of separate interfaces instead. +// Instead of having an a single event/result with accompanying payload values +// for both host -> plugin `dispatcher()` and plugin -> host `audioMaster()` +// calls, we'll send objects of type `T` that should receive a response of type +// `T::Response`, where all of the possible `T`s are stored in an +// `std::variant`. This way we communicate in a completely type safe way. + +// TODO: If this approach works, maybe we can also refactor the VST2 handling to +// do this since it's a bit safer and easier to read + +// All messages for creating objects and calling interfaces on them are defined +// as part of the interfaces and implementations in `vst3/` + +/** + * Marker struct to indicate the other side (the plugin) should send a copy of + * the configuration. + */ +struct WantsConfiguration { + using Response = Configuration; + + template + void serialize(S&) {} +}; + +/** + * When we send a control message from the plugin to the Wine VST host, this + * encodes the information we request or the operation we want to perform. A + * request of type `ControlRequest(T)` should send back a `T::Response`. + */ +using ControlRequest = std::variant; + +template +void serialize(S& s, ControlRequest& payload) { + // All of the objects in `ControlRequest` should have their own + // serialization function. + s.ext(payload, bitsery::ext::StdVariant{}); +} + +/** + * A subset of all functions a host can call on a plugin. These functions are + * called from a hot loop every processing cycle, so we want a dedicated socket + * for these for every plugin instance. + */ +using AudioProcessorRequest = + std::variant; + +template +void serialize(S& s, AudioProcessorRequest& payload) { + // All of the objects in `AudioProcessorRequest` should have their own + // serialization function. + s.ext(payload, bitsery::ext::StdVariant{}); +} + +/** + * When we do a callback from the Wine VST host to the plugin, this encodes the + * information we want or the operation we want to perform. A request of type + * `CallbackRequest(T)` should send back a `T::Response`. + */ +using CallbackRequest = std::variant; + +template +void serialize(S& s, CallbackRequest& payload) { + // All of the objects in `CallbackRequest` should have their own + // serialization function. + s.ext(payload, bitsery::ext::StdVariant{}); +} diff --git a/src/common/serialization/vst3/README.md b/src/common/serialization/vst3/README.md new file mode 100644 index 00000000..32056749 --- /dev/null +++ b/src/common/serialization/vst3/README.md @@ -0,0 +1,52 @@ +# VST3 interfaces + +We currently support all VST 3.0.0 interfaces. See +[docs/vst3.md](https://github.com/robbert-vdh/yabridge/blob/master/docs/vst3.md) +for more information on how the serialization works. + +VST3 plugin interfaces are implemented as follows: + +| yabridge class | Included in | Interfaces | +| -------------------------- | ------------------- | ------------------------------------------------------ | +| `YaPluginFactory` | | `IPluginFactory`, `IPluginFactory2`, `IPluginFactory3` | +| `Vst3ConnectionPointProxy` | | `IConnectionPoint` through `YaConnectionPoint` | +| `Vst3PlugViewProxy` | | All of the below: | +| `YaPlugView` | `Vst3PlugViewProxy` | `IPlugView` | +| `Vst3PluginProxy` | | All of the below: | +| `YaAudioProcessor` | `Vst3PluginProxy` | `IAudioProcessor` | +| `YaComponent` | `Vst3PluginProxy` | `IComponent` | +| `YaConnectionPoint` | `Vst3PluginProxy` | `IConnectionPoint` | +| `YaEditController` | `Vst3PluginProxy` | `IEditController` | +| `YaEditController2` | `Vst3PluginProxy` | `IEditController2` | +| `YaPluginBase` | `Vst3PluginProxy` | `IPluginBase` | +| `YaProgramListData` | `Vst3PluginProxy` | `IProgramListData` | +| `YaUnitData` | `Vst3PluginProxy` | `IUnitData` | +| `YaUnitInfo` | `Vst3PluginProxy` | `IUnitInfo` | + +VST3 host interfaces are implemented as follows: + +| yabridge class | Included in | Interfaces | +| --------------------------- | --------------------------- | ------------------- | +| `Vst3HostContextProxy` | | All of the below: | +| `YaHostApplication` | `Vst3HostContextProxy` | `IHostApplication` | +| `Vst3ComponentHandlerProxy` | | All of the below: | +| `YaComponentHandler` | `Vst3ComponentHandlerProxy` | `IComponentHandler` | +| `YaUnitHandler` | `Vst3ComponentHandlerProxy` | `IUnitHandler` | +| `Vst3PlugFrameProxy` | | All of the below: | +| `YaPlugFrame` | `Vst3PlugFrameProxy` | `IPlugFrame` | + +The following host interfaces are passed as function arguments and are thus also +implemented for serialization purposes: + +| yabridge class | Interfaces | Notes | +| -------------------- | ------------------- | ---------------------------------------------------------------------- | +| `YaAttributeList` | `IAttributeList` | | +| `YaEventList` | `IEventList` | Comes with a lot of serialization wrappers around the related structs. | +| `YaMessage` | `IMessage` | | +| `YaMessagePtr` | `IMessage` | Should be used in inter process communication to exchange messages | +| `YaParameterChanges` | `IParameterChanges` | | +| `YaParamValueQueue` | `IParamValueQueue` | | +| `VectorStream` | `IBStream` | Used for serializing data streams. | + +And finally `YaProcessData` uses the above along with `YaAudioBusBuffers` to +wrap around `ProcessData`. diff --git a/src/common/serialization/vst3/attribute-list.cpp b/src/common/serialization/vst3/attribute-list.cpp new file mode 100644 index 00000000..33311fe3 --- /dev/null +++ b/src/common/serialization/vst3/attribute-list.cpp @@ -0,0 +1,115 @@ +// 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 . + +#include "attribute-list.h" + +YaAttributeList::YaAttributeList(){FUNKNOWN_CTOR} + +YaAttributeList::~YaAttributeList() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_FUNKNOWN_METHODS(YaAttributeList, + Steinberg::Vst::IAttributeList, + Steinberg::Vst::IAttributeList::iid) +#pragma GCC diagnostic pop + +tresult PLUGIN_API YaAttributeList::setInt(AttrID id, int64 value) { + attrs_int[id] = value; + return Steinberg::kResultOk; +} + +tresult PLUGIN_API YaAttributeList::getInt(AttrID id, int64& value) { + if (const auto it = attrs_int.find(id); it != attrs_int.end()) { + value = it->second; + return Steinberg::kResultOk; + } else { + return Steinberg::kResultFalse; + } +} + +tresult PLUGIN_API YaAttributeList::setFloat(AttrID id, double value) { + attrs_float[id] = value; + return Steinberg::kResultOk; +} + +tresult PLUGIN_API YaAttributeList::getFloat(AttrID id, double& value) { + if (const auto it = attrs_float.find(id); it != attrs_float.end()) { + value = it->second; + return Steinberg::kResultOk; + } else { + return Steinberg::kResultFalse; + } +} + +tresult PLUGIN_API +YaAttributeList::setString(AttrID id, const Steinberg::Vst::TChar* string) { + if (!string) { + return Steinberg::kInvalidArgument; + } + + attrs_string[id] = tchar_pointer_to_u16string(string); + return Steinberg::kResultOk; +} + +tresult PLUGIN_API YaAttributeList::getString(AttrID id, + Steinberg::Vst::TChar* string, + uint32 sizeInBytes) { + if (!string) { + return Steinberg::kInvalidArgument; + } + + if (const auto it = attrs_string.find(id); it != attrs_string.end()) { + // We may only copy `sizeInBytes / 2` UTF-16 characters to `string`, + // We'll also have to make sure it's null terminated, so we'll reserve + // another byte for that. + const size_t copy_characters = std::min( + (static_cast(sizeInBytes) / sizeof(Steinberg::Vst::TChar)) - + 1, + it->second.size()); + std::copy_n(it->second.begin(), copy_characters, string); + string[copy_characters] = 0; + + return Steinberg::kResultOk; + } else { + return Steinberg::kResultFalse; + } +} + +tresult PLUGIN_API YaAttributeList::setBinary(AttrID id, + const void* data, + uint32 sizeInBytes) { + if (!data) { + return Steinberg::kInvalidArgument; + } + + const uint8_t* data_bytes = static_cast(data); + attrs_binary[id].assign(data_bytes, data_bytes + sizeInBytes); + return Steinberg::kResultOk; +} +tresult PLUGIN_API YaAttributeList::getBinary(AttrID id, + const void*& data, + uint32& sizeInBytes) { + if (const auto it = attrs_binary.find(id); it != attrs_binary.end()) { + data = it->second.data(); + sizeInBytes = it->second.size(); + return Steinberg::kResultOk; + } else { + return Steinberg::kResultFalse; + } +} diff --git a/src/common/serialization/vst3/attribute-list.h b/src/common/serialization/vst3/attribute-list.h new file mode 100644 index 00000000..12f816de --- /dev/null +++ b/src/common/serialization/vst3/attribute-list.h @@ -0,0 +1,90 @@ +// 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 . + +#pragma once + +#include + +#include +#include +#include + +#include "base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IAttributeList` for storing parameters in `YaMessage`. + */ +class YaAttributeList : public Steinberg::Vst::IAttributeList { + public: + /** + * Default constructor with an empty attributeList. The plugin can use this + * to write a attributeList. + */ + YaAttributeList(); + + ~YaAttributeList(); + + DECLARE_FUNKNOWN_METHODS + + virtual tresult PLUGIN_API setInt(AttrID id, int64 value) override; + virtual tresult PLUGIN_API getInt(AttrID id, int64& value) override; + virtual tresult PLUGIN_API setFloat(AttrID id, double value) override; + virtual tresult PLUGIN_API getFloat(AttrID id, double& value) override; + virtual tresult PLUGIN_API + setString(AttrID id, const Steinberg::Vst::TChar* string) override; + virtual tresult PLUGIN_API getString(AttrID id, + Steinberg::Vst::TChar* string, + uint32 sizeInBytes) override; + virtual tresult PLUGIN_API setBinary(AttrID id, + const void* data, + uint32 sizeInBytes) override; + virtual tresult PLUGIN_API getBinary(AttrID id, + const void*& data, + uint32& sizeInBytes) override; + + template + void serialize(S& s) { + s.ext(attrs_int, bitsery::ext::StdMap{1 << 20}, + [](S& s, std::string& key, int64& value) { + s.text1b(key, 1024); + s.value8b(value); + }); + s.ext(attrs_float, bitsery::ext::StdMap{1 << 20}, + [](S& s, std::string& key, double& value) { + s.text1b(key, 1024); + s.value8b(value); + }); + s.ext(attrs_string, bitsery::ext::StdMap{1 << 20}, + [](S& s, std::string& key, std::u16string& value) { + s.text1b(key, 1024); + s.text2b(value, 1 << 20); + }); + s.ext(attrs_binary, bitsery::ext::StdMap{1 << 20}, + [](S& s, std::string& key, std::vector& value) { + s.text1b(key, 1024); + s.container1b(value, 1 << 20); + }); + } + + private: + std::unordered_map attrs_int; + std::unordered_map attrs_float; + std::unordered_map attrs_string; + std::unordered_map> attrs_binary; +}; diff --git a/src/common/serialization/vst3/base.cpp b/src/common/serialization/vst3/base.cpp new file mode 100644 index 00000000..dabaf68e --- /dev/null +++ b/src/common/serialization/vst3/base.cpp @@ -0,0 +1,330 @@ +// 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 . + +#include +#include +#include +#include + +#include "base.h" + +std::string format_uid(const Steinberg::FUID& uid) { + // This is the same as `FUID::print`, but without any macro prefixes + uint32 l1, l2, l3, l4; + uid.to4Int(l1, l2, l3, l4); + + std::ostringstream formatted_uid; + formatted_uid << std::hex << std::uppercase << "{0x" << std::setfill('0') + << std::setw(8) << l1 << ", 0x" << std::setfill('0') + << std::setw(8) << l2 << ", 0x" << std::setfill('0') + << std::setw(8) << l3 << ", 0x" << std::setfill('0') + << std::setw(8) << l4 << "}"; + + return formatted_uid.str(); +} + +std::u16string tchar_pointer_to_u16string(const Steinberg::Vst::TChar* string) { +#ifdef __WINE__ + // This is great, thanks Steinberg + static_assert(sizeof(Steinberg::Vst::TChar) == sizeof(char16_t)); + return std::u16string(reinterpret_cast(string)); +#else + return std::u16string(static_cast(string)); +#endif +} + +std::u16string tchar_pointer_to_u16string(const Steinberg::Vst::TChar* string, + uint32 length) { +#ifdef __WINE__ + static_assert(sizeof(Steinberg::Vst::TChar) == sizeof(char16_t)); + return std::u16string(reinterpret_cast(string), length); +#else + return std::u16string(static_cast(string), length); +#endif +} + +const Steinberg::Vst::TChar* u16string_to_tchar_pointer( + const std::u16string& string) { +#ifdef __WINE__ + static_assert(sizeof(Steinberg::Vst::TChar) == sizeof(char16_t)); + return reinterpret_cast(string.c_str()); +#else + return static_cast(string.c_str()); +#endif +} + +UniversalTResult::UniversalTResult() : universal_result(Value::kResultFalse) {} + +UniversalTResult::UniversalTResult(tresult native_result) + : universal_result(to_universal_result(native_result)) {} + +UniversalTResult::operator tresult() const { + static_assert(Steinberg::kResultOk == Steinberg::kResultTrue); + switch (universal_result) { + case Value::kNoInterface: + return Steinberg::kNoInterface; + break; + case Value::kResultOk: + return Steinberg::kResultOk; + break; + case Value::kResultFalse: + return Steinberg::kResultFalse; + break; + case Value::kInvalidArgument: + return Steinberg::kInvalidArgument; + break; + case Value::kNotImplemented: + return Steinberg::kNotImplemented; + break; + case Value::kInternalError: + return Steinberg::kInternalError; + break; + case Value::kNotInitialized: + return Steinberg::kNotInitialized; + break; + case Value::kOutOfMemory: + return Steinberg::kOutOfMemory; + break; + default: + // Shouldn't be happening + return Steinberg::kInvalidArgument; + break; + } +} + +std::string UniversalTResult::string() const { + static_assert(Steinberg::kResultOk == Steinberg::kResultTrue); + switch (universal_result) { + case Value::kNoInterface: + return "kNoInterface"; + break; + case Value::kResultOk: + return "kResultOk"; + break; + case Value::kResultFalse: + return "kResultFalse"; + break; + case Value::kInvalidArgument: + return "kInvalidArgument"; + break; + case Value::kNotImplemented: + return "kNotImplemented"; + break; + case Value::kInternalError: + return "kInternalError"; + break; + case Value::kNotInitialized: + return "kNotInitialized"; + break; + case Value::kOutOfMemory: + return "kOutOfMemory"; + break; + default: + return ""; + break; + } +} + +UniversalTResult::Value UniversalTResult::to_universal_result( + tresult native_result) { + static_assert(Steinberg::kResultOk == Steinberg::kResultTrue); + switch (native_result) { + case Steinberg::kNoInterface: + return Value::kNoInterface; + break; + case Steinberg::kResultOk: + return Value::kResultOk; + break; + case Steinberg::kResultFalse: + return Value::kResultFalse; + break; + case Steinberg::kInvalidArgument: + return Value::kInvalidArgument; + break; + case Steinberg::kNotImplemented: + return Value::kNotImplemented; + break; + case Steinberg::kInternalError: + return Value::kInternalError; + break; + case Steinberg::kNotInitialized: + return Value::kNotInitialized; + break; + case Steinberg::kOutOfMemory: + return Value::kOutOfMemory; + break; + default: + // Shouldn't be happening + return Value::kInvalidArgument; + break; + } +} + +VectorStream::VectorStream(){FUNKNOWN_CTOR} + +VectorStream::VectorStream(Steinberg::IBStream* stream) { + FUNKNOWN_CTOR + + if (!stream) { + throw std::runtime_error("Null pointer passed to VectorStream()"); + } + + if (stream->seek(0, Steinberg::IBStream::IStreamSeekMode::kIBSeekEnd) != + Steinberg::kResultOk) { + throw std::runtime_error( + "IBStream passed to VectorStream() does not suport seeking to end"); + } + + // Now that we're at the end of the stream we know how large the buffer + // should be + int64 size; + assert(stream->tell(&size) == Steinberg::kResultOk); + + int32 num_bytes_read = 0; + buffer.resize(size); + assert(stream->seek(0, Steinberg::IBStream::IStreamSeekMode::kIBSeekSet) == + Steinberg::kResultOk); + assert(stream->read(buffer.data(), size, &num_bytes_read) == + Steinberg::kResultOk); + assert(num_bytes_read == 0 || num_bytes_read == size); +} + +VectorStream::~VectorStream() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_REFCOUNT(VectorStream) +#pragma GCC diagnostic pop + +tresult PLUGIN_API VectorStream::queryInterface(Steinberg::FIDString _iid, + void** obj) { + QUERY_INTERFACE(_iid, obj, Steinberg::FUnknown::iid, Steinberg::IBStream) + QUERY_INTERFACE(_iid, obj, Steinberg::IBStream::iid, Steinberg::IBStream) + QUERY_INTERFACE(_iid, obj, Steinberg::ISizeableStream::iid, + Steinberg::ISizeableStream) + + *obj = nullptr; + return Steinberg::kNoInterface; +} + +tresult VectorStream::write_back(Steinberg::IBStream* stream) const { + if (!stream) { + return Steinberg::kInvalidArgument; + } + + // A `stream->seek(0, kIBSeekSet)` breaks restoring states in Bitwig. Not + // sure if Bitwig is prepending a header or if this is expected behaviour. + int32 num_bytes_written = 0; + if (stream->write(const_cast(buffer.data()), buffer.size(), + &num_bytes_written) == Steinberg::kResultOk) { + // Some implementations will return `kResultFalse` when writing 0 bytes + assert(num_bytes_written == 0 || + static_cast(num_bytes_written) == buffer.size()); + } + + return Steinberg::kResultOk; +} + +size_t VectorStream::size() const { + return buffer.size(); +} + +tresult PLUGIN_API VectorStream::read(void* buffer, + int32 numBytes, + int32* numBytesRead) { + if (!buffer || numBytes < 0) { + return Steinberg::kInvalidArgument; + } + + size_t bytes_to_read = std::min(static_cast(numBytes), + this->buffer.size() - seek_position); + + std::copy_n(&this->buffer[seek_position], bytes_to_read, + reinterpret_cast(buffer)); + + seek_position += bytes_to_read; + if (numBytesRead) { + *numBytesRead = bytes_to_read; + } + + return Steinberg::kResultOk; +} + +tresult PLUGIN_API VectorStream::write(void* buffer, + int32 numBytes, + int32* numBytesWritten) { + if (!buffer || numBytes < 0) { + return Steinberg::kInvalidArgument; + } + + if (seek_position + numBytes > this->buffer.size()) { + this->buffer.resize(seek_position + numBytes); + } + + std::copy_n(reinterpret_cast(buffer), numBytes, + this->buffer.begin() + seek_position); + + seek_position += numBytes; + if (numBytesWritten) { + *numBytesWritten = numBytes; + } + + return Steinberg::kResultOk; +} + +tresult PLUGIN_API VectorStream::seek(int64 pos, int32 mode, int64* result) { + switch (mode) { + case kIBSeekSet: + seek_position = pos; + break; + case kIBSeekCur: + seek_position += pos; + break; + case kIBSeekEnd: + seek_position = this->buffer.size() + pos; + break; + default: + return Steinberg::kInvalidArgument; + break; + } + + if (result) { + *result = seek_position; + } + + return Steinberg::kResultOk; +} + +tresult PLUGIN_API VectorStream::tell(int64* pos) { + if (pos) { + *pos = seek_position; + return Steinberg::kResultOk; + } else { + return Steinberg::kInvalidArgument; + } +} + +tresult PLUGIN_API VectorStream::getStreamSize(int64& size) { + size = seek_position; + return Steinberg::kResultOk; +} + +tresult PLUGIN_API VectorStream::setStreamSize(int64 size) { + buffer.resize(size); + return Steinberg::kResultOk; +} diff --git a/src/common/serialization/vst3/base.h b/src/common/serialization/vst3/base.h new file mode 100644 index 00000000..8b0bfeb2 --- /dev/null +++ b/src/common/serialization/vst3/base.h @@ -0,0 +1,231 @@ +// 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 . + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +// Yet Another layer of includes, but these are some VST3-specific typedefs that +// we'll need for all of our interfaces + +using Steinberg::TBool, Steinberg::char16, Steinberg::int8, Steinberg::int16, + Steinberg::int32, Steinberg::int64, Steinberg::uint8, Steinberg::uint16, + Steinberg::uint32, Steinberg::uint64, Steinberg::tresult; + +/** + * Both `TUID` (`int8_t[16]`) and `FIDString` (`char*`) are hard to work with + * because you can't just copy them. So when serializing/deserializing them + * we'll use `std::array`. + */ +using ArrayUID = std::array< + std::remove_reference_t()[0])>, + std::extent_v>; + +/** + * The maximum number of speakers or busses we support. + */ +constexpr size_t max_num_speakers = 16384; + +/** + * The maximum size for an `IBStream` we can serialize. Allows for up to 50 MB + * of preset data. Hopefully no plugin will come anywhere near this limit, but + * it will add up when plugins start to include audio samples in their presets. + */ +constexpr size_t max_vector_stream_size = 50 << 20; + +/** + * Format a FUID as a simple hexadecimal four-tuple. + */ +std::string format_uid(const Steinberg::FUID& uid); + +/** + * Convert a UTF-16 C-style string to an `std::u16string`. Who event invented + * UTF-16? + */ +std::u16string tchar_pointer_to_u16string(const Steinberg::Vst::TChar* string); + +/** + * Same as the above, but with a fixed string length. + * + * @overload + */ +std::u16string tchar_pointer_to_u16string(const Steinberg::Vst::TChar* string, + uint32 length); + +/** + * Convert an `std::u16string` back to a null terminated `TChar*` string. + */ +const Steinberg::Vst::TChar* u16string_to_tchar_pointer( + const std::u16string& string); + +/** + * Empty struct for when we have send a response to some operation without any + * result values. + */ +struct Ack { + template + void serialize(S&) {} +}; + +/** + * A simple wrapper around primitive values for serialization purposes. Bitsery + * doesn't seem to like serializing plain primitives using `s.object()` even if + * you define a serialization function. + */ +template +class PrimitiveWrapper { + public: + PrimitiveWrapper() {} + PrimitiveWrapper(T value) : value(value) {} + + operator T() const { return value; } + + template + void serialize(S& s) { + s.template value(value); + } + + private: + T value; +}; + +/** + * A wrapper around `Steinberg::tresult` that we can safely share between the + * native plugin and the Wine process. Depending on the platform and on whether + * or not the VST3 SDK is compiled to be COM compatible, the result codes may + * have three different values for the same meaning. + */ +class UniversalTResult { + public: + /** + * The default constructor will initialize the value to `kResutlFalse` and + * should only ever be used by bitsery in the serialization process. + */ + UniversalTResult(); + + /** + * Convert a native tresult into a univeral one. + */ + UniversalTResult(tresult native_result); + + /** + * Get the native equivalent for the wrapped `tresult` value. + */ + operator tresult() const; + + /** + * Get the original name for the result, e.g. `kResultOk`. + */ + std::string string() const; + + template + void serialize(S& s) { + s.value4b(universal_result); + } + + private: + /** + * These are the non-COM compatible values copied from + * ` The actual values h ere don't matter + * but hopefully the compiler can be a bit smarter about it this way. + */ + enum class Value { + kNoInterface = -1, + kResultOk, + kResultTrue = kResultOk, + kResultFalse, + kInvalidArgument, + kNotImplemented, + kInternalError, + kNotInitialized, + kOutOfMemory + }; + + static Value to_universal_result(tresult native_result); + + Value universal_result; +}; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Serialize an `IBStream` into an `std::vector`, and allow the + * receiving side to use it as an `IBStream` again. `ISizeableStream` is defined + * but then for whatever reason never used, but we'll implement it anyways. + */ +class VectorStream : public Steinberg::IBStream, + public Steinberg::ISizeableStream { + public: + VectorStream(); + + /** + * Read an existing stream. + * + * @throw std::runtime_error If we couldn't read from the stream. + */ + VectorStream(Steinberg::IBStream* stream); + + virtual ~VectorStream(); + + DECLARE_FUNKNOWN_METHODS + + /** + * Write the vector buffer back to an IBStream. After writing the seek + * position will be left at the end of the stream. + */ + tresult write_back(Steinberg::IBStream* stream) const; + + /** + * Return the buffer's, used in the logging messages. + */ + size_t size() const; + + // From `IBstream` + tresult PLUGIN_API read(void* buffer, + int32 numBytes, + int32* numBytesRead = nullptr) override; + tresult PLUGIN_API write(void* buffer, + int32 numBytes, + int32* numBytesWritten = nullptr) override; + tresult PLUGIN_API seek(int64 pos, + int32 mode, + int64* result = nullptr) override; + tresult PLUGIN_API tell(int64* pos) override; + + // From `ISizeableStream` + tresult PLUGIN_API getStreamSize(int64& size) override; + tresult PLUGIN_API setStreamSize(int64 size) override; + + template + void serialize(S& s) { + s.container1b(buffer, max_vector_stream_size); + // The seek position should always be initialized at 0 + } + + private: + std::vector buffer; + size_t seek_position = 0; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/component-handler-proxy.cpp b/src/common/serialization/vst3/component-handler-proxy.cpp new file mode 100644 index 00000000..45753acb --- /dev/null +++ b/src/common/serialization/vst3/component-handler-proxy.cpp @@ -0,0 +1,58 @@ +// 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 . + +#include "component-handler-proxy.h" + +Vst3ComponentHandlerProxy::ConstructArgs::ConstructArgs() {} + +Vst3ComponentHandlerProxy::ConstructArgs::ConstructArgs( + Steinberg::IPtr object, + size_t owner_instance_id) + : owner_instance_id(owner_instance_id), + component_handler_args(object), + unit_handler_args(object) {} + +Vst3ComponentHandlerProxy::Vst3ComponentHandlerProxy(const ConstructArgs&& args) + : YaComponentHandler(std::move(args.component_handler_args)), + YaUnitHandler(std::move(args.unit_handler_args)), + arguments(std::move(args)){FUNKNOWN_CTOR} + + Vst3ComponentHandlerProxy::~Vst3ComponentHandlerProxy() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_REFCOUNT(Vst3ComponentHandlerProxy) +#pragma GCC diagnostic pop + +tresult PLUGIN_API +Vst3ComponentHandlerProxy::queryInterface(Steinberg::FIDString _iid, + void** obj) { + if (YaComponentHandler::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::FUnknown::iid, + Steinberg::Vst::IComponentHandler) + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IComponentHandler::iid, + Steinberg::Vst::IComponentHandler) + } + if (YaUnitHandler::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IUnitHandler::iid, + Steinberg::Vst::IUnitHandler) + } + + *obj = nullptr; + return Steinberg::kNoInterface; +} diff --git a/src/common/serialization/vst3/component-handler-proxy.h b/src/common/serialization/vst3/component-handler-proxy.h new file mode 100644 index 00000000..0c82cea4 --- /dev/null +++ b/src/common/serialization/vst3/component-handler-proxy.h @@ -0,0 +1,103 @@ +// 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 . + +#pragma once + +#include "../common.h" +#include "component-handler/component-handler.h" +#include "component-handler/unit-handler.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * An abstract class that implements `IComponentHandler`, and optionally also + * all other VST3 interfaces an object passed to + * `IEditController::setComponentHandler()` might implement. This works exactly + * the same as `Vst3PluginProxy`, but instead of proxying for an object provided + * by the plugin we are proxying for the `IComponentHandler*` argument passed to + * plugin by the host. + */ +class Vst3ComponentHandlerProxy : public YaComponentHandler, + public YaUnitHandler { + public: + /** + * These are the arguments for constructing a + * `Vst3ComponentHandlerProxyImpl`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Read from an existing object. We will try to mimic this object, so + * we'll support any interfaces this object also supports. + */ + ConstructArgs(Steinberg::IPtr object, + size_t owner_instance_id); + + /** + * The unique instance identifier of the proxy object instance this + * component handler has been passed to and thus belongs to. This way we + * can refer to the correct 'actual' `IComponentHandler` instance when + * the plugin does a callback. + */ + native_size_t owner_instance_id; + + YaComponentHandler::ConstructArgs component_handler_args; + YaUnitHandler::ConstructArgs unit_handler_args; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.object(component_handler_args); + s.object(unit_handler_args); + } + }; + + /** + * Instantiate this instance with arguments read from an actual component + * handler. + * + * @note Since this is passed as part of + * `IEditController::setComponentHandler()`, there are no direct + * `Construct` or `Destruct` messages. This object's lifetime is bound to + * that of the objects they are passed to. If those objects get dropped, + * then the host contexts should also be dropped. + */ + Vst3ComponentHandlerProxy(const ConstructArgs&& args); + + /** + * The lifetime of this object should be bound to the object we created it + * for. When for instance the `Vst3PluginProxy` instance with id `n` gets + * dropped a corresponding `Vst3ComponentHandlerProxyImpl` should also be + * dropped. + */ + virtual ~Vst3ComponentHandlerProxy(); + + DECLARE_FUNKNOWN_METHODS + + /** + * Get the instance ID of the owner of this object. + */ + inline size_t owner_instance_id() const { + return arguments.owner_instance_id; + } + + private: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/component-handler/component-handler.cpp b/src/common/serialization/vst3/component-handler/component-handler.cpp new file mode 100644 index 00000000..a9967efd --- /dev/null +++ b/src/common/serialization/vst3/component-handler/component-handler.cpp @@ -0,0 +1,27 @@ +// 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 . + +#include "component-handler.h" + +YaComponentHandler::ConstructArgs::ConstructArgs() {} + +YaComponentHandler::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported( + Steinberg::FUnknownPtr(object)) {} + +YaComponentHandler::YaComponentHandler(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/component-handler/component-handler.h b/src/common/serialization/vst3/component-handler/component-handler.h new file mode 100644 index 00000000..a55f8144 --- /dev/null +++ b/src/common/serialization/vst3/component-handler/component-handler.h @@ -0,0 +1,154 @@ +// 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 . + +#pragma once + +#include + +#include "../../common.h" +#include "../base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IComponentHandler` for serialization purposes. This is + * instantiated as part of `Vst3ComponentHandlerProxy`. + */ +class YaComponentHandler : public Steinberg::Vst::IComponentHandler { + public: + /** + * These are the arguments for creating a `YaComponentHandler`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements + * `IComponentHandler` and read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaComponentHandler(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to `IComponentHandler::beginEdit(id)` to + * the component handler provided by the host. + */ + struct BeginEdit { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + Steinberg::Vst::ParamID id; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value4b(id); + } + }; + + virtual tresult PLUGIN_API + beginEdit(Steinberg::Vst::ParamID id) override = 0; + + /** + * Message to pass through a call to `IComponentHandler::performEdit(id, + * value_normalized)` to the component handler provided by the host. + */ + struct PerformEdit { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + Steinberg::Vst::ParamID id; + Steinberg::Vst::ParamValue value_normalized; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value4b(id); + s.value8b(value_normalized); + } + }; + + virtual tresult PLUGIN_API + performEdit(Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue valueNormalized) override = 0; + + /** + * Message to pass through a call to `IComponentHandler::endEdit(id)` to the + * component handler provided by the host. + */ + struct EndEdit { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + Steinberg::Vst::ParamID id; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value4b(id); + } + }; + + virtual tresult PLUGIN_API endEdit(Steinberg::Vst::ParamID id) override = 0; + + /** + * Message to pass through a call to + * `IComponentHandler::restartComponent(flags)` to the component handler + * provided by the host. + */ + struct RestartComponent { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + int32 flags; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value4b(flags); + } + }; + + virtual tresult PLUGIN_API restartComponent(int32 flags) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/component-handler/unit-handler.cpp b/src/common/serialization/vst3/component-handler/unit-handler.cpp new file mode 100644 index 00000000..cd04034e --- /dev/null +++ b/src/common/serialization/vst3/component-handler/unit-handler.cpp @@ -0,0 +1,26 @@ +// 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 . + +#include "unit-handler.h" + +YaUnitHandler::ConstructArgs::ConstructArgs() {} + +YaUnitHandler::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported(Steinberg::FUnknownPtr(object)) {} + +YaUnitHandler::YaUnitHandler(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/component-handler/unit-handler.h b/src/common/serialization/vst3/component-handler/unit-handler.h new file mode 100644 index 00000000..21e82cfe --- /dev/null +++ b/src/common/serialization/vst3/component-handler/unit-handler.h @@ -0,0 +1,115 @@ +// 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 . + +#pragma once + +#include + +#include "../../common.h" +#include "../base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IUnitHandler` for serialization purposes. This is instantiated + * as part of `Vst3UnitHandlerProxy`. + */ +class YaUnitHandler : public Steinberg::Vst::IUnitHandler { + public: + /** + * These are the arguments for creating a `YaUnitHandler`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements `IUnitHandler` + * and read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaUnitHandler(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to + * `IUnitHandler::notifyUnitSelection(unit_id)` to the unit handler provided + * by the host. + */ + struct NotifyUnitSelection { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + Steinberg::Vst::UnitID unit_id; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value4b(unit_id); + } + }; + + virtual tresult PLUGIN_API + notifyUnitSelection(Steinberg::Vst::UnitID unitId) override = 0; + + /** + * Message to pass through a call to + * `IUnitHandler::notifyProgramListChange(list_id, program_index)` to the + * unit handler provided by the host. + */ + struct NotifyProgramListChange { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + Steinberg::Vst::ProgramListID list_id; + int32 program_index; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value4b(list_id); + s.value4b(program_index); + } + }; + + virtual tresult PLUGIN_API + notifyProgramListChange(Steinberg::Vst::ProgramListID listId, + int32 programIndex) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/connection-point-proxy.cpp b/src/common/serialization/vst3/connection-point-proxy.cpp new file mode 100644 index 00000000..5800c21b --- /dev/null +++ b/src/common/serialization/vst3/connection-point-proxy.cpp @@ -0,0 +1,44 @@ +// 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 . + +#include "connection-point-proxy.h" + +Vst3ConnectionPointProxy::Vst3ConnectionPointProxy(const ConstructArgs&& args) + : YaConnectionPoint(std::move(args.connection_point_args)), + arguments(std::move(args)){FUNKNOWN_CTOR} + + Vst3ConnectionPointProxy::~Vst3ConnectionPointProxy() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_REFCOUNT(Vst3ConnectionPointProxy) +#pragma GCC diagnostic pop + +tresult PLUGIN_API +Vst3ConnectionPointProxy::queryInterface(Steinberg::FIDString _iid, + void** obj) { + if (YaConnectionPoint::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::FUnknown::iid, + Steinberg::Vst::IConnectionPoint) + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IConnectionPoint::iid, + Steinberg::Vst::IConnectionPoint) + } + + *obj = nullptr; + return Steinberg::kNoInterface; +} diff --git a/src/common/serialization/vst3/connection-point-proxy.h b/src/common/serialization/vst3/connection-point-proxy.h new file mode 100644 index 00000000..f6585625 --- /dev/null +++ b/src/common/serialization/vst3/connection-point-proxy.h @@ -0,0 +1,74 @@ +// 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 . + +#pragma once + +#include "../common.h" +#include "plugin/connection-point.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * This is only needed to...proxy a connection point proxy. Most hosts will + * connect a plugin's processor and controller directly using + * `IConnectionPoint::connect()`. But some hosts, like Ardour, will place a + * proxy object between them that forwards calls to + * `IConnectionPoint::notify()`. When objects are connected directly by the host + * we can also connect them directly in the Wine plugin host, but when the host + * uses proxies we'll also have to go through that proxy. The purpose of this + * class is to provide a proxy for such a connection proxy. So when the plugin + * calls `notify()` on an object of this class, then we will forward that call + * to the `IConnectionPoint` proxy provided by the host, which will then in turn + * call `IConnectionPoint::notify()` on the other object and we'll then forward + * that message again to them Wine plugin host. + */ +class Vst3ConnectionPointProxy : public YaConnectionPoint { + public: + // We had to define this in `YaConnectionPoint` to work around circular + // includes + using ConstructArgs = + YaConnectionPoint::Vst3ConnectionPointProxyConstructArgs; + + /** + * Instantiate this instance with arguments read from an actual + * `IConnectionPoint` object/proxy. + * + * @note This object will be created as part of handling + * `IConnectionPoint::connect()` if the connection is indirect. + */ + Vst3ConnectionPointProxy(const ConstructArgs&& args); + + /** + * This object will be destroyed again during + * `IConnectionPoint::disconnect()`. + */ + virtual ~Vst3ConnectionPointProxy(); + + DECLARE_FUNKNOWN_METHODS + + /** + * Get the instance ID of the owner of this object. + */ + inline size_t owner_instance_id() const { + return arguments.owner_instance_id; + } + + private: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/event-list.cpp b/src/common/serialization/vst3/event-list.cpp new file mode 100644 index 00000000..da882533 --- /dev/null +++ b/src/common/serialization/vst3/event-list.cpp @@ -0,0 +1,248 @@ +// 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 . + +#include "event-list.h" + +#include "src/common/utils.h" + +YaDataEvent::YaDataEvent() {} + +YaDataEvent::YaDataEvent(const Steinberg::Vst::DataEvent& event) + : type(event.type), buffer(event.bytes, event.bytes + event.size) {} + +Steinberg::Vst::DataEvent YaDataEvent::get() const { + return Steinberg::Vst::DataEvent{.size = static_cast(buffer.size()), + .type = type, + .bytes = buffer.data()}; +} + +YaNoteExpressionTextEvent::YaNoteExpressionTextEvent() {} + +YaNoteExpressionTextEvent::YaNoteExpressionTextEvent( + const Steinberg::Vst::NoteExpressionTextEvent& event) + : type_id(event.typeId), + note_id(event.noteId), + text(tchar_pointer_to_u16string(event.text, event.textLen)) {} + +Steinberg::Vst::NoteExpressionTextEvent YaNoteExpressionTextEvent::get() const { + return Steinberg::Vst::NoteExpressionTextEvent{ + .typeId = type_id, + .noteId = note_id, + .textLen = static_cast(text.size()), + .text = u16string_to_tchar_pointer(text)}; +} + +YaChordEvent::YaChordEvent() {} + +YaChordEvent::YaChordEvent(const Steinberg::Vst::ChordEvent& event) + : root(event.root), + bass_note(event.bassNote), + mask(event.mask), + text(tchar_pointer_to_u16string(event.text, event.textLen)) {} + +Steinberg::Vst::ChordEvent YaChordEvent::get() const { + return Steinberg::Vst::ChordEvent{ + .root = root, + .bassNote = bass_note, + .mask = mask, + .textLen = static_cast(text.size()), + .text = u16string_to_tchar_pointer(text)}; +} + +YaScaleEvent::YaScaleEvent() {} + +YaScaleEvent::YaScaleEvent(const Steinberg::Vst::ScaleEvent& event) + : root(event.root), + mask(event.mask), + text(tchar_pointer_to_u16string(event.text, event.textLen)) {} + +Steinberg::Vst::ScaleEvent YaScaleEvent::get() const { + return Steinberg::Vst::ScaleEvent{ + .root = root, + .mask = mask, + .textLen = static_cast(text.size()), + .text = u16string_to_tchar_pointer(text)}; +} + +YaEvent::YaEvent() {} + +YaEvent::YaEvent(const Steinberg::Vst::Event& event) + : bus_index(event.busIndex), + sample_offset(event.sampleOffset), + ppq_position(event.ppqPosition), + flags(event.flags) { + // Now we need the correct event type + switch (event.type) { + case Steinberg::Vst::Event::kNoteOnEvent: + payload = event.noteOn; + break; + case Steinberg::Vst::Event::kNoteOffEvent: + payload = event.noteOff; + break; + case Steinberg::Vst::Event::kDataEvent: + payload = YaDataEvent(event.data); + break; + case Steinberg::Vst::Event::kPolyPressureEvent: + payload = event.polyPressure; + break; + case Steinberg::Vst::Event::kNoteExpressionValueEvent: + payload = event.noteExpressionValue; + break; + case Steinberg::Vst::Event::kNoteExpressionTextEvent: + payload = YaNoteExpressionTextEvent(event.noteExpressionText); + break; + case Steinberg::Vst::Event::kChordEvent: + payload = YaChordEvent(event.chord); + break; + case Steinberg::Vst::Event::kScaleEvent: + payload = YaScaleEvent(event.scale); + break; + case Steinberg::Vst::Event::kLegacyMIDICCOutEvent: + payload = event.midiCCOut; + break; + default: + // XXX: When encountering something we don't know about, should we + // throw or silently ignore it? We can't properly log about + // this directly from here. + break; + } +} + +Steinberg::Vst::Event YaEvent::get() const { + // We of course can't fully initialize a field with an untagged union +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + Steinberg::Vst::Event event{.busIndex = bus_index, + .sampleOffset = sample_offset, + .ppqPosition = ppq_position, + .flags = flags}; +#pragma GCC diagnostic pop + std::visit( + overload{ + [&](const Steinberg::Vst::NoteOnEvent& specific_event) { + event.type = Steinberg::Vst::Event::kNoteOnEvent; + event.noteOn = specific_event; + }, + [&](const Steinberg::Vst::NoteOffEvent& specific_event) { + event.type = Steinberg::Vst::Event::kNoteOffEvent; + event.noteOff = specific_event; + }, + [&](const YaDataEvent& specific_event) { + event.type = Steinberg::Vst::Event::kDataEvent; + event.data = specific_event.get(); + }, + [&](const Steinberg::Vst::PolyPressureEvent& specific_event) { + event.type = Steinberg::Vst::Event::kPolyPressureEvent; + event.polyPressure = specific_event; + }, + [&](const Steinberg::Vst::NoteExpressionValueEvent& + specific_event) { + event.type = Steinberg::Vst::Event::kNoteExpressionValueEvent; + event.noteExpressionValue = specific_event; + }, + [&](const YaNoteExpressionTextEvent& specific_event) { + event.type = Steinberg::Vst::Event::kNoteExpressionTextEvent; + event.noteExpressionText = specific_event.get(); + }, + [&](const YaChordEvent& specific_event) { + event.type = Steinberg::Vst::Event::kChordEvent; + event.chord = specific_event.get(); + }, + [&](const YaScaleEvent& specific_event) { + event.type = Steinberg::Vst::Event::kScaleEvent; + event.scale = specific_event.get(); + }, + [&](const Steinberg::Vst::LegacyMIDICCOutEvent& specific_event) { + event.type = Steinberg::Vst::Event::kLegacyMIDICCOutEvent; + event.midiCCOut = specific_event; + }}, + payload); + + return event; +} + +YaEventList::YaEventList(){FUNKNOWN_CTOR} + +YaEventList::YaEventList(Steinberg::Vst::IEventList& event_list) { + FUNKNOWN_CTOR + + events.reserve(event_list.getEventCount()); + + // Copy over all events. Everything gets converted to `YaEvent`s. + Steinberg::Vst::Event event; + for (int i = 0; i < event_list.getEventCount(); i++) { + // We're skipping the `kResultOk` assertions here + event_list.getEvent(i, event); + events.push_back(event); + } +} + +YaEventList::~YaEventList(){FUNKNOWN_DTOR} + +size_t YaEventList::num_events() const { + return events.size(); +} + +void YaEventList::write_back_outputs( + Steinberg::Vst::IEventList& output_events) const { + // TODO: I assume the host is responsible for directly copying heap data + // (e.g. text) in these events and they're not supposed to stay + // around, right? If not, then we'll find out very quickly. + for (auto& event : events) { + Steinberg::Vst::Event reconstructed_event = event.get(); + output_events.addEvent(reconstructed_event); + } +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_FUNKNOWN_METHODS(YaEventList, + Steinberg::Vst::IEventList, + Steinberg::Vst::IEventList::iid) +#pragma GCC diagnostic pop + +int32 PLUGIN_API YaEventList::getEventCount() { + return events.size(); +} + +tresult PLUGIN_API YaEventList::getEvent(int32 index, + Steinberg::Vst::Event& e /*out*/) { + if (index < 0 || index >= static_cast(events.size())) { + return Steinberg::kInvalidArgument; + } + + // On the first call to this, we'll reconstruct `Event` objects out of our + // `YaEvent`s all at once. This is also done if for whatever reason the + // plugin `getEvent()`s an event it just added. + const size_t num_already_reconstructed_events = reconstructed_events.size(); + if (index >= static_cast(num_already_reconstructed_events)) { + reconstructed_events.resize(events.size()); + std::transform( + events.begin() + num_already_reconstructed_events, events.end(), + reconstructed_events.begin() + num_already_reconstructed_events, + [](const YaEvent& event) { return event.get(); }); + } + + e = reconstructed_events[index]; + + return Steinberg::kResultOk; +} + +tresult PLUGIN_API YaEventList::addEvent(Steinberg::Vst::Event& e /*in*/) { + events.push_back(e); + + return Steinberg::kResultOk; +} diff --git a/src/common/serialization/vst3/event-list.h b/src/common/serialization/vst3/event-list.h new file mode 100644 index 00000000..ce052bea --- /dev/null +++ b/src/common/serialization/vst3/event-list.h @@ -0,0 +1,309 @@ +// 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 . + +#pragma once + +#include +#include + +#include "base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * A wrapper around `DataEvent` for serialization purposes, as this event + * contains a heap array. + */ +struct YaDataEvent { + YaDataEvent(); + + /** + * Copy data from an existing `DataEvent`. + */ + YaDataEvent(const Steinberg::Vst::DataEvent& event); + + /** + * Reconstruct a `DataEvent` from this object. + * + * @note This object may contain pointers to data stored in this object, and + * must thus not outlive it. + */ + Steinberg::Vst::DataEvent get() const; + + uint32 type; + std::vector buffer; + + template + void serialize(S& s) { + s.value4b(type); + s.container1b(buffer, 1 << 16); + } +}; + +/** + * A wrapper around `NoteExpressionTextEvent` for serialization purposes, as + * this event contains a heap array. + */ +struct YaNoteExpressionTextEvent { + YaNoteExpressionTextEvent(); + + /** + * Copy data from an existing `NoteExpressionTextEvent`. + */ + YaNoteExpressionTextEvent( + const Steinberg::Vst::NoteExpressionTextEvent& event); + + /** + * Reconstruct a `NoteExpressionTextEvent` from this object. + * + * @note This object may contain pointers to data stored in this object, and + * must thus not outlive it. + */ + Steinberg::Vst::NoteExpressionTextEvent get() const; + + Steinberg::Vst::NoteExpressionTypeID type_id; + int32 note_id; + + std::u16string text; + + template + void serialize(S& s) { + s.value4b(type_id); + s.value4b(note_id); + s.container2b(text, 1 << 16); + } +}; + +/** + * A wrapper around `ChordEvent` for serialization purposes, as this event + * contains a heap array. + */ +struct YaChordEvent { + YaChordEvent(); + + /** + * Copy data from an existing `ChordEvent`. + */ + YaChordEvent(const Steinberg::Vst::ChordEvent& event); + + /** + * Reconstruct a `ChordEvent` from this object. + * + * @note This object may contain pointers to data stored in this object, and + * must thus not outlive it. + */ + Steinberg::Vst::ChordEvent get() const; + + int16 root; + int16 bass_note; + int16 mask; + + std::u16string text; + + template + void serialize(S& s) { + s.value2b(root); + s.value2b(bass_note); + s.value2b(mask); + s.container2b(text, 1 << 16); + } +}; + +/** + * A wrapper around `ScaleEvent` for serialization purposes, as this event + * contains a heap array. + */ +struct YaScaleEvent { + YaScaleEvent(); + + /** + * Copy data from an existing `ScaleEvent`. + */ + YaScaleEvent(const Steinberg::Vst::ScaleEvent& event); + + /** + * Reconstruct a `ScaleEvent` from this object. + * + * @note This object may contain pointers to data stored in this object, and + * must thus not outlive it. + */ + Steinberg::Vst::ScaleEvent get() const; + + int16 root; + int16 mask; + + std::u16string text; + + template + void serialize(S& s) { + s.value2b(root); + s.value2b(mask); + s.container2b(text, 1 << 16); + } +}; + +/** + * A wrapper around `Event` for serialization purposes, as some event types + * include heap pointers. + */ +struct YaEvent { + YaEvent(); + + /** + * Copy data from an `Event`. + */ + YaEvent(const Steinberg::Vst::Event& event); + + /** + * Reconstruct an `Event` from this object. + * + * @note This object may contain pointers to data stored in this object, and + * must thus not outlive it. + */ + Steinberg::Vst::Event get() const; + + // These fields directly reflect those from `Event` + int32 bus_index; + int32 sample_offset; + Steinberg::Vst::TQuarterNotes ppq_position; + uint16 flags; + + // `Event` stores an event type and a union, we'll encode both in a variant. + // We can use simple types directly, and we need serializable wrappers + // around move event types with heap pointers. + std::variant + payload; + + template + void serialize(S& s) { + s.value4b(bus_index); + s.value4b(sample_offset); + s.value8b(ppq_position); + s.value2b(flags); + s.ext(payload, bitsery::ext::StdVariant{}); + } +}; + +/** + * Wraps around `IEventList` for serialization purposes. Used in + * `YaProcessData`. + */ +class YaEventList : public Steinberg::Vst::IEventList { + public: + /** + * Default constructor with an empty event list. The plugin can use this to + * output data. + */ + YaEventList(); + + /** + * Read data from an existing `IEventList` object. + */ + YaEventList(Steinberg::Vst::IEventList& event_list); + + ~YaEventList(); + + DECLARE_FUNKNOWN_METHODS + + /** + * Return the number of events we store. Used in debug logs. + */ + size_t num_events() const; + + /** + * Write these events an output events queue on the `ProcessData` object + * provided by the host. + */ + void write_back_outputs(Steinberg::Vst::IEventList& output_events) const; + + // From `IEventList` + virtual int32 PLUGIN_API getEventCount() override; + virtual tresult PLUGIN_API + getEvent(int32 index, Steinberg::Vst::Event& e /*out*/) override; + virtual tresult PLUGIN_API + addEvent(Steinberg::Vst::Event& e /*in*/) override; + + template + void serialize(S& s) { + s.container(events, 1 << 16); + } + + private: + std::vector events; + + /** + * On the first `getEvent()` call we'll reconstruct these from `events` all + * at once. These event objects may not outlive this event list. + */ + std::vector reconstructed_events; +}; + +namespace Steinberg { +namespace Vst { +template +void serialize(S& s, NoteOnEvent& event) { + s.value2b(event.channel); + s.value2b(event.pitch); + s.value4b(event.tuning); + s.value4b(event.velocity); + s.value4b(event.length); + s.value4b(event.noteId); +} + +template +void serialize(S& s, NoteOffEvent& event) { + s.value2b(event.channel); + s.value2b(event.pitch); + s.value4b(event.velocity); + s.value4b(event.noteId); + s.value4b(event.tuning); +} + +template +void serialize(S& s, PolyPressureEvent& event) { + s.value2b(event.channel); + s.value2b(event.pitch); + s.value4b(event.pressure); + s.value4b(event.noteId); +} + +template +void serialize(S& s, NoteExpressionValueEvent& event) { + s.value4b(event.typeId); + s.value4b(event.noteId); + s.value8b(event.value); +} + +template +void serialize(S& s, LegacyMIDICCOutEvent& event) { + s.value1b(event.controlNumber); + s.value1b(event.channel); + s.value1b(event.value); + s.value1b(event.value2); +} +} // namespace Vst +} // namespace Steinberg + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/host-context-proxy.cpp b/src/common/serialization/vst3/host-context-proxy.cpp new file mode 100644 index 00000000..1e32b6ab --- /dev/null +++ b/src/common/serialization/vst3/host-context-proxy.cpp @@ -0,0 +1,50 @@ +// 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 . + +#include "host-context-proxy.h" + +Vst3HostContextProxy::ConstructArgs::ConstructArgs() {} + +Vst3HostContextProxy::ConstructArgs::ConstructArgs( + Steinberg::IPtr object, + std::optional owner_instance_id) + : owner_instance_id(owner_instance_id), host_application_args(object) {} + +Vst3HostContextProxy::Vst3HostContextProxy(const ConstructArgs&& args) + : YaHostApplication(std::move(args.host_application_args)), + arguments(std::move(args)){FUNKNOWN_CTOR} + + Vst3HostContextProxy::~Vst3HostContextProxy() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_REFCOUNT(Vst3HostContextProxy) +#pragma GCC diagnostic pop + +tresult PLUGIN_API +Vst3HostContextProxy::queryInterface(Steinberg::FIDString _iid, void** obj) { + if (YaHostApplication::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::FUnknown::iid, + Steinberg::Vst::IHostApplication) + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IHostApplication::iid, + Steinberg::Vst::IHostApplication) + } + + *obj = nullptr; + return Steinberg::kNoInterface; +} diff --git a/src/common/serialization/vst3/host-context-proxy.h b/src/common/serialization/vst3/host-context-proxy.h new file mode 100644 index 00000000..e2afe641 --- /dev/null +++ b/src/common/serialization/vst3/host-context-proxy.h @@ -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 . + +#pragma once + +#include "../common.h" +#include "host-context/host-application.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * An abstract class that optionally implements all interfaces a `context` + * object passed to `IPluginBase::intialize()` or + * `IPluginFactory3::setHostContext()` might implement. This works exactly the + * same as `Vst3PluginProxy`, but instead of proxying for an object provided by + * the plugin we are proxying for the `FUnknown*` argument passed to plugin by + * the host. When we are proxying for a host context object passed to + * `IPluginBase::initialize()` we'll keep track of the object instance ID the + * actual context object belongs to. + */ +class Vst3HostContextProxy : public YaHostApplication { + public: + /** + * These are the arguments for constructing a + * `Vst3HostContextProxyImpl`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Read from an existing object. We will try to mimic this object, so + * we'll support any interfaces this object also supports. + */ + ConstructArgs(Steinberg::IPtr object, + std::optional owner_instance_id); + + /** + * The unique instance identifier of the proxy object instance this host + * context has been passed to and thus belongs to. If we are handling + * When handling `IPluginFactory::setHostContext()` this will be empty. + */ + std::optional owner_instance_id; + + YaHostApplication::ConstructArgs host_application_args; + + template + void serialize(S& s) { + s.ext(owner_instance_id, bitsery::ext::StdOptional{}, + [](S& s, native_size_t& instance_id) { + s.value8b(instance_id); + }); + s.object(host_application_args); + } + }; + + /** + * Instantiate this instance with arguments read from an actual host + * context. + * + * @note Since this is passed as part of `IPluginBase::intialize()` and + * `IPluginFactory3::setHostContext()`, there are no direct `Construct` or + * `Destruct` messages. This object's lifetime is bound to that of the + * objects they are passed to. If those objects get dropped, then the host + * contexts should also be dropped. + */ + Vst3HostContextProxy(const ConstructArgs&& args); + + /** + * The lifetime of this object should be bound to the object we created it + * for. When for instance the `Vst3PluginProxy` instance with id `n` gets + * dropped a corresponding `Vst3HostContextProxyImpl` should also be + * dropped. + */ + virtual ~Vst3HostContextProxy(); + + DECLARE_FUNKNOWN_METHODS + + /** + * Get the instance ID of the owner of this object, if this is not the + * global host context passed to the module's plugin factory. + */ + inline std::optional owner_instance_id() const { + return arguments.owner_instance_id; + } + + private: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/host-context/host-application.cpp b/src/common/serialization/vst3/host-context/host-application.cpp new file mode 100644 index 00000000..46c12993 --- /dev/null +++ b/src/common/serialization/vst3/host-context/host-application.cpp @@ -0,0 +1,27 @@ +// 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 . + +#include "host-application.h" + +YaHostApplication::ConstructArgs::ConstructArgs() {} + +YaHostApplication::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported( + Steinberg::FUnknownPtr(object)) {} + +YaHostApplication::YaHostApplication(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/host-context/host-application.h b/src/common/serialization/vst3/host-context/host-application.h new file mode 100644 index 00000000..5b63dea4 --- /dev/null +++ b/src/common/serialization/vst3/host-context/host-application.h @@ -0,0 +1,116 @@ +// 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 . + +#pragma once + +#include + +#include +#include +#include + +#include "../../common.h" +#include "../base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IHostApplication` for serialization purposes. This is + * instantiated as part of `Vst3HostContextProxy`. + */ +class YaHostApplication : public Steinberg::Vst::IHostApplication { + public: + /** + * These are the arguments for creating a `YaHostApplication`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements + * `IHostApplication` and read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaHostApplication(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * The response code and resulting value for a call to + * `IHostApplication::getName()`. + */ + struct GetNameResponse { + UniversalTResult result; + std::u16string name; + + template + void serialize(S& s) { + s.object(result); + s.text2b(name, std::extent_v); + } + }; + + /** + * Message to pass through a call to `IHostApplication::getName()` to the + * host context provided by the host. + */ + struct GetName { + using Response = GetNameResponse; + + /** + * The object instance whose host context to call this function to. Of + * empty, then the function will be called on the factory's host context + * instead. + */ + std::optional owner_instance_id; + + template + void serialize(S& s) { + s.ext(owner_instance_id, bitsery::ext::StdOptional{}, + [](S& s, native_size_t& instance_id) { + s.value8b(instance_id); + }); + } + }; + + virtual tresult PLUGIN_API + getName(Steinberg::Vst::String128 name) override = 0; + virtual tresult PLUGIN_API createInstance(Steinberg::TUID cid, + Steinberg::TUID _iid, + void** obj) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/message.cpp b/src/common/serialization/vst3/message.cpp new file mode 100644 index 00000000..ac285a2a --- /dev/null +++ b/src/common/serialization/vst3/message.cpp @@ -0,0 +1,96 @@ +// 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 . + +#include "message.h" + +YaMessagePtr::YaMessagePtr(){FUNKNOWN_CTOR} + +YaMessagePtr::YaMessagePtr(IMessage& message) + : message_id(message.getMessageID() + ? std::make_optional(message.getMessageID()) + : std::nullopt), + original_message_ptr(static_cast( + reinterpret_cast(&message))){FUNKNOWN_CTOR} + + YaMessagePtr::~YaMessagePtr() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_FUNKNOWN_METHODS(YaMessagePtr, + Steinberg::Vst::IMessage, + Steinberg::Vst::IMessage::iid) +#pragma GCC diagnostic pop + +Steinberg::Vst::IMessage* YaMessagePtr::get_original() const { + // See the docstrings on `YaMessage` and `YaMessagePtr` + return reinterpret_cast( + static_cast(original_message_ptr)); +} + +Steinberg::FIDString PLUGIN_API YaMessagePtr::getMessageID() { + if (message_id) { + return message_id->c_str(); + } else { + return nullptr; + } +} + +void PLUGIN_API YaMessagePtr::setMessageID(Steinberg::FIDString id /*in*/) { + if (id) { + message_id = id; + } else { + message_id.reset(); + } +} + +Steinberg::Vst::IAttributeList* PLUGIN_API YaMessagePtr::getAttributes() { + return &attribute_list; +} + +YaMessage::YaMessage(){FUNKNOWN_CTOR} + +YaMessage::~YaMessage() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_FUNKNOWN_METHODS(YaMessage, + Steinberg::Vst::IMessage, + Steinberg::Vst::IMessage::iid) +#pragma GCC diagnostic pop + +Steinberg::FIDString PLUGIN_API YaMessage::getMessageID() { + if (message_id) { + return message_id->c_str(); + } else { + return nullptr; + } +} + +void PLUGIN_API YaMessage::setMessageID(Steinberg::FIDString id /*in*/) { + if (id) { + message_id = id; + } else { + message_id.reset(); + } +} + +Steinberg::Vst::IAttributeList* PLUGIN_API YaMessage::getAttributes() { + return &attribute_list; +} diff --git a/src/common/serialization/vst3/message.h b/src/common/serialization/vst3/message.h new file mode 100644 index 00000000..a4e1aa6c --- /dev/null +++ b/src/common/serialization/vst3/message.h @@ -0,0 +1,144 @@ +// 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 . + +#pragma once + +#include +#include + +#include "../common.h" +#include "attribute-list.h" +#include "base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * A serialization wrapper around `IMessage`. As explained in `YaMessage`, we + * can't exchange the regular `YaMessage` object when dealing with + * `IConnectionPoint` connection proxies. Instead, we'll use this wrapper that + * only stores the ID (for logging purposes) and a pointer to the original + * object. That way we can pass the original message created by the plugin to + * the receiver without having to know what object the host's connection proxy + * is actually connecting us to. + * + * @note THis object should _not_ be passed to the plugin directly. The only + * purpose of this object is to be able to pass the original `IMessage*` + * object passed the connection proxy to the receiver, by wrapping a pointer + * to it in this object. `YaMessagePtr::get_original()` can be used to + * retrieve the original object. + */ +class YaMessagePtr : public Steinberg::Vst::IMessage { + public: + YaMessagePtr(); + + /** + * Create a proxy for this message. We'll store the message's ID for logging + * purposes as well as a pointer to it so we can retrieve the object after a + * round trip from the Wine plugin host, to the native plugin, to the host, + * back to the native plugin, and then finally back to the Wine plugin host + * again. + */ + explicit YaMessagePtr(IMessage& message); + + ~YaMessagePtr(); + + DECLARE_FUNKNOWN_METHODS + + /** + * Get back a pointer to the original `IMessage` object passed to the + * constructor. This should be used on the Wine plugin host side when + * handling `IConnectionPoint::notify`. + */ + Steinberg::Vst::IMessage* get_original() const; + + virtual Steinberg::FIDString PLUGIN_API getMessageID() override; + virtual void PLUGIN_API + setMessageID(Steinberg::FIDString id /*in*/) override; + virtual Steinberg::Vst::IAttributeList* PLUGIN_API getAttributes() override; + + template + void serialize(S& s) { + s.ext(message_id, bitsery::ext::StdOptional{}, + [](S& s, std::string& id) { s.text1b(id, 1024); }); + s.value8b(original_message_ptr); + } + + private: + /** + * The implementation that comes with the SDK returns a null pointer when + * the ID has not yet been set, so we'll do the same thing. + */ + std::optional message_id; + + /** + * The pointer to the message passed during the constructor, as a 64-bit + * unsigned integer. This way we can retrieve the original object after a + * round trip. + */ + native_size_t original_message_ptr = 0; + + /** + * An empty attribute list, in case the host checks this for some reason. + */ + YaAttributeList attribute_list; +}; + +/** + * A `IMessage` implementation the plugin can use to exchange messages with. We + * create instances of these in `IHostApplication::createInstance()` so the + * Windows VST3 plugin can send messages between objects. A plugin's controller + * or processor will fill the message with data and then try to send it to the + * connected object using `IConnectionPoint::notify()`. For directly connected + * objects this works exactly like you'd expect. When the host places a proxy + * between the two, it becomes a bit more interesting, and we'll have to proxy + * that proxy. In that case we won't send the actual `YaMessage` object from the + * Wine plugin host to the native plugin, and then back to the Wine plugin host. + * Instead, we'll send a thin wrapper that only stores a name and a pointer to + * the actual object. This is needed in case the plugin tries to store the + * `IMessage` object, thinking it's backed by a smart pointer. This means that + * the message we pass while handling `IConnectionPoint::notify` should live as + * long as the original message object, thus we'll use a pointer to get back the + * original message object. + * + * @relates YaMessagePtr + */ +class YaMessage : public Steinberg::Vst::IMessage { + public: + /** + * Default constructor with an empty message. The plugin can use this to + * write a message. + */ + YaMessage(); + + ~YaMessage(); + + DECLARE_FUNKNOWN_METHODS + + virtual Steinberg::FIDString PLUGIN_API getMessageID() override; + virtual void PLUGIN_API + setMessageID(Steinberg::FIDString id /*in*/) override; + virtual Steinberg::Vst::IAttributeList* PLUGIN_API getAttributes() override; + + private: + /** + * The implementation that comes with the SDK returns a null pointer when + * the ID has not yet been set, so we'll do the same thing. + */ + std::optional message_id; + + YaAttributeList attribute_list; +}; diff --git a/src/common/serialization/vst3/param-value-queue.cpp b/src/common/serialization/vst3/param-value-queue.cpp new file mode 100644 index 00000000..45c4897a --- /dev/null +++ b/src/common/serialization/vst3/param-value-queue.cpp @@ -0,0 +1,88 @@ +// 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 . + +#include "param-value-queue.h" + +YaParamValueQueue::YaParamValueQueue(){FUNKNOWN_CTOR} + +YaParamValueQueue::YaParamValueQueue(Steinberg::Vst::ParamID parameter_id) + : parameter_id(parameter_id){FUNKNOWN_CTOR} + + // clang-format /really/ doesn't like these macros + YaParamValueQueue::YaParamValueQueue(Steinberg::Vst::IParamValueQueue & + original_queue) + : parameter_id(original_queue.getParameterId()), + queue(original_queue.getPointCount()) { + FUNKNOWN_CTOR + + // Copy over all points to our vector + for (int i = 0; i < original_queue.getPointCount(); i++) { + // We're skipping the assertions here and just assume that the function + // returns `kResultOk` + original_queue.getPoint(i, queue[i].first, queue[i].second); + } +} + +YaParamValueQueue::~YaParamValueQueue() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_FUNKNOWN_METHODS(YaParamValueQueue, + Steinberg::Vst::IParamValueQueue, + Steinberg::Vst::IParamValueQueue::iid) +#pragma GCC diagnostic pop + +void YaParamValueQueue::write_back_outputs( + Steinberg::Vst::IParamValueQueue& output_queue) const { + // We don't need this value + int32 index; + for (const auto& [sample_offset, value] : queue) { + // We don't check for `kResultOk` here + output_queue.addPoint(sample_offset, value, index); + } +} + +Steinberg::Vst::ParamID PLUGIN_API YaParamValueQueue::getParameterId() { + return parameter_id; +} + +int32 PLUGIN_API YaParamValueQueue::getPointCount() { + return queue.size(); +} + +tresult PLUGIN_API +YaParamValueQueue::getPoint(int32 index, + int32& sampleOffset /*out*/, + Steinberg::Vst::ParamValue& value /*out*/) { + if (index < static_cast(queue.size())) { + sampleOffset = queue[index].first; + value = queue[index].second; + + return Steinberg::kResultOk; + } else { + return Steinberg::kInvalidArgument; + } +} +tresult PLUGIN_API YaParamValueQueue::addPoint(int32 sampleOffset, + Steinberg::Vst::ParamValue value, + int32& index /*out*/) { + index = queue.size(); + queue.push_back({sampleOffset, value}); + + return Steinberg::kResultOk; +} diff --git a/src/common/serialization/vst3/param-value-queue.h b/src/common/serialization/vst3/param-value-queue.h new file mode 100644 index 00000000..dfb13464 --- /dev/null +++ b/src/common/serialization/vst3/param-value-queue.h @@ -0,0 +1,99 @@ +// 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 . + +#pragma once + +#include +#include + +#include "base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IParamValueQueue` for serializing a queue containing changes to + * a single parameter during the current processing cycle. Used in + * `YaParameterChanges`. + */ +class YaParamValueQueue : public Steinberg::Vst::IParamValueQueue { + public: + /** + * Default constructor with an empty queue. + */ + YaParamValueQueue(); + + /** + * Create an empty queue for a specific parameter. Used in + * `YaParameterChanges::addParameterData`. + */ + YaParamValueQueue(Steinberg::Vst::ParamID parameter_id); + + /** + * Read data from an existing `IParamValueQueue` object. + */ + YaParamValueQueue(Steinberg::Vst::IParamValueQueue& original_queue); + + ~YaParamValueQueue(); + + DECLARE_FUNKNOWN_METHODS + + /** + * Write this queue back the output parameter changes object on the + * `ProcessData` object provided by the host. + */ + void write_back_outputs( + Steinberg::Vst::IParamValueQueue& output_queue) const; + + // From `IParamValueQueue` + Steinberg::Vst::ParamID PLUGIN_API getParameterId() override; + int32 PLUGIN_API getPointCount() override; + tresult PLUGIN_API + getPoint(int32 index, + int32& sampleOffset /*out*/, + Steinberg::Vst::ParamValue& value /*out*/) override; + tresult PLUGIN_API addPoint(int32 sampleOffset, + Steinberg::Vst::ParamValue value, + int32& index /*out*/) override; + + template + void serialize(S& s) { + s.value4b(parameter_id); + // TODO: Does bitsery have a built in way to serialize pairs? + s.container(queue, 1 << 16, [](S& s, std::pair& pair) { + s.value4b(pair.first); + s.value8b(pair.second); + }); + } + + /** + * For `IParamValueQueue::getParameterId`. + */ + Steinberg::Vst::ParamID parameter_id; + + private: + /** + * The actual parameter changes queue. The specification doesn't mention + * that this should be a priority queue or something, but I'd assume both + * the plugin and the host will insert the values in chronological order + * (because, why would they not?). + * + * This contains pairs of `(sample_offset, value)`. + */ + std::vector> queue; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/parameter-changes.cpp b/src/common/serialization/vst3/parameter-changes.cpp new file mode 100644 index 00000000..83b8ed50 --- /dev/null +++ b/src/common/serialization/vst3/parameter-changes.cpp @@ -0,0 +1,81 @@ +// 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 . + +#include "parameter-changes.h" + +YaParameterChanges::YaParameterChanges(){FUNKNOWN_CTOR} + +YaParameterChanges::YaParameterChanges( + Steinberg::Vst::IParameterChanges& original_queues) { + FUNKNOWN_CTOR + + // Copy over all parameter changne queues. Everything gets converted to + // `YaParamValueQueue`s. + queues.reserve(original_queues.getParameterCount()); + for (int i = 0; i < original_queues.getParameterCount(); i++) { + queues.push_back(*original_queues.getParameterData(i)); + } +} + +YaParameterChanges::~YaParameterChanges() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_FUNKNOWN_METHODS(YaParameterChanges, + Steinberg::Vst::IParameterChanges, + Steinberg::Vst::IParameterChanges::iid) +#pragma GCC diagnostic pop + +size_t YaParameterChanges::num_parameters() const { + return queues.size(); +} + +void YaParameterChanges::write_back_outputs( + Steinberg::Vst::IParameterChanges& output_queues) const { + for (auto& queue : queues) { + // We don't need this, but the SDK requires us to need this + int32 output_queue_index; + if (Steinberg::Vst::IParamValueQueue* output_queue = + output_queues.addParameterData(queue.parameter_id, + output_queue_index)) { + queue.write_back_outputs(*output_queue); + } + } +} + +int32 PLUGIN_API YaParameterChanges::getParameterCount() { + return queues.size(); +} + +Steinberg::Vst::IParamValueQueue* PLUGIN_API +YaParameterChanges::getParameterData(int32 index) { + if (index < static_cast(queues.size())) { + return &queues[index]; + } else { + return nullptr; + } +} + +Steinberg::Vst::IParamValueQueue* PLUGIN_API +YaParameterChanges::addParameterData(const Steinberg::Vst::ParamID& id, + int32& index /*out*/) { + index = queues.size(); + queues.push_back(YaParamValueQueue(id)); + + return &queues[index]; +} diff --git a/src/common/serialization/vst3/parameter-changes.h b/src/common/serialization/vst3/parameter-changes.h new file mode 100644 index 00000000..83fc3a3e --- /dev/null +++ b/src/common/serialization/vst3/parameter-changes.h @@ -0,0 +1,81 @@ +// 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 . + +#pragma once + +#include + +#include "base.h" +#include "param-value-queue.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IParameterChanges` for serialization purposes. Used in + * `YaProcessData`. + */ +class YaParameterChanges : public Steinberg::Vst::IParameterChanges { + public: + /** + * Default constructor with an empty parameter changes list. The plugin can + * use this to output data. + */ + YaParameterChanges(); + + /** + * Read data from an existing `IParameterChanges` object. + */ + YaParameterChanges(Steinberg::Vst::IParameterChanges& original_queues); + + ~YaParameterChanges(); + + DECLARE_FUNKNOWN_METHODS + + /** + * Return the number of parameter we have parameter change queues for. Used + * in debug logs. + */ + size_t num_parameters() const; + + /** + * Write these changes back to an output parameter changes queue on the + * `ProcessData` object provided by the host. + */ + void write_back_outputs( + Steinberg::Vst::IParameterChanges& output_queues) const; + + // From `IParameterChanges` + int32 PLUGIN_API getParameterCount() override; + Steinberg::Vst::IParamValueQueue* PLUGIN_API + getParameterData(int32 index) override; + Steinberg::Vst::IParamValueQueue* PLUGIN_API + addParameterData(const Steinberg::Vst::ParamID& id, + int32& index /*out*/) override; + + template + void serialize(S& s) { + s.container(queues, 1 << 16); + } + + private: + /** + * The parameter value changes queues. + */ + std::vector queues; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/plug-frame-proxy.cpp b/src/common/serialization/vst3/plug-frame-proxy.cpp new file mode 100644 index 00000000..abf391c7 --- /dev/null +++ b/src/common/serialization/vst3/plug-frame-proxy.cpp @@ -0,0 +1,50 @@ +// 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 . + +#include "plug-frame-proxy.h" + +Vst3PlugFrameProxy::ConstructArgs::ConstructArgs() {} + +Vst3PlugFrameProxy::ConstructArgs::ConstructArgs( + Steinberg::IPtr object, + size_t owner_instance_id) + : owner_instance_id(owner_instance_id), plug_frame_args(object) {} + +Vst3PlugFrameProxy::Vst3PlugFrameProxy(const ConstructArgs&& args) + : YaPlugFrame(std::move(args.plug_frame_args)), + arguments(std::move(args)){FUNKNOWN_CTOR} + + Vst3PlugFrameProxy::~Vst3PlugFrameProxy() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_REFCOUNT(Vst3PlugFrameProxy) +#pragma GCC diagnostic pop + +tresult PLUGIN_API Vst3PlugFrameProxy::queryInterface(Steinberg::FIDString _iid, + void** obj) { + if (YaPlugFrame::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::FUnknown::iid, + Steinberg::IPlugFrame) + QUERY_INTERFACE(_iid, obj, Steinberg::IPlugFrame::iid, + Steinberg::IPlugFrame) + } + + *obj = nullptr; + return Steinberg::kNoInterface; +} diff --git a/src/common/serialization/vst3/plug-frame-proxy.h b/src/common/serialization/vst3/plug-frame-proxy.h new file mode 100644 index 00000000..0a45bcb3 --- /dev/null +++ b/src/common/serialization/vst3/plug-frame-proxy.h @@ -0,0 +1,97 @@ +// 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 . + +#pragma once + +#include "../common.h" +#include "plug-frame/plug-frame.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * An abstract class that implements `IPlugFrame`, and optionally also all other + * VST3 interfaces an object passed to `IPlugView::setFrame()` might implement. + * This works exactly the same as `Vst3PluginProxy`, but instead of proxying for + * an object provided by the plugin we are proxying for the `IPlugFrame*` + * argument passed to plugin by the host. + */ +class Vst3PlugFrameProxy : public YaPlugFrame { + public: + /** + * These are the arguments for constructing a + * `Vst3PlugFrameProxyImpl`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Read from an existing object. We will try to mimic this object, so + * we'll support any interfaces this object also supports. + */ + ConstructArgs(Steinberg::IPtr object, + size_t owner_instance_id); + + /** + * The unique instance identifier of the proxy object instance this + * component handler has been passed to and thus belongs to. This way we + * can refer to the correct 'actual' `IPlugFrame` instance when the + * plugin does a callback. + */ + native_size_t owner_instance_id; + + YaPlugFrame::ConstructArgs plug_frame_args; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.object(plug_frame_args); + } + }; + + /** + * Instantiate this instance with arguments read from an actual component + * handler. + * + * @note Since this is passed as part of `IEditController::setPlugFrame()`, + * there are no direct `Construct` or `Destruct` messages. This object's + * lifetime is bound to that of the objects they are passed to. If the + * plug view instance gets dropped, this proxy should also be dropped. + */ + Vst3PlugFrameProxy(const ConstructArgs&& args); + + /** + * The lifetime of this object should be bound to the object we created it + * for. When the `Vst3PlugViewProxy` for the object with instance with id + * `n` gets dropped, the corresponding `Vst3PlugFrameProxy` should also be + * dropped. + */ + virtual ~Vst3PlugFrameProxy(); + + DECLARE_FUNKNOWN_METHODS + + /** + * Get the instance ID of the owner of this object. + */ + inline size_t owner_instance_id() const { + return arguments.owner_instance_id; + } + + private: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/plug-frame/plug-frame.cpp b/src/common/serialization/vst3/plug-frame/plug-frame.cpp new file mode 100644 index 00000000..a1cc9d46 --- /dev/null +++ b/src/common/serialization/vst3/plug-frame/plug-frame.cpp @@ -0,0 +1,26 @@ +// 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 . + +#include "plug-frame.h" + +YaPlugFrame::ConstructArgs::ConstructArgs() {} + +YaPlugFrame::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported(Steinberg::FUnknownPtr(object)) {} + +YaPlugFrame::YaPlugFrame(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plug-frame/plug-frame.h b/src/common/serialization/vst3/plug-frame/plug-frame.h new file mode 100644 index 00000000..89504638 --- /dev/null +++ b/src/common/serialization/vst3/plug-frame/plug-frame.h @@ -0,0 +1,94 @@ +// 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 . + +#pragma once + +#include + +#include "../../common.h" +#include "../base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IPlugFrame` for serialization purposes. This is instantiated as + * part of `Vst3PlugFrameProxy`. + */ +class YaPlugFrame : public Steinberg::IPlugFrame { + public: + /** + * These are the arguments for creating a `YaPlugFrame`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements `IPlugFrame` and + * read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaPlugFrame(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to `IPlugFrame::resizeView` to the + * `IPlugView` object provided by the host. + * + * XXX: Since we don't support multiple `IPlugView`s right now (as it's not + * used the SDK's current version), we'll just assume that `view` is + * the view stored in `Vst3PluginProxyImpl::plug_view` + */ + struct ResizeView { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + Steinberg::ViewRect new_size; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.object(new_size); + } + }; + + virtual tresult PLUGIN_API + resizeView(Steinberg::IPlugView* view, + Steinberg::ViewRect* newSize) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/plug-view-proxy.cpp b/src/common/serialization/vst3/plug-view-proxy.cpp new file mode 100644 index 00000000..de2fdf49 --- /dev/null +++ b/src/common/serialization/vst3/plug-view-proxy.cpp @@ -0,0 +1,50 @@ +// 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 . + +#include "plug-view-proxy.h" + +Vst3PlugViewProxy::ConstructArgs::ConstructArgs() {} + +Vst3PlugViewProxy::ConstructArgs::ConstructArgs( + Steinberg::IPtr object, + size_t owner_instance_id) + : owner_instance_id(owner_instance_id), plug_view_args(object) {} + +Vst3PlugViewProxy::Vst3PlugViewProxy(const ConstructArgs&& args) + : YaPlugView(std::move(args.plug_view_args)), + arguments(std::move(args)){FUNKNOWN_CTOR} + + Vst3PlugViewProxy::~Vst3PlugViewProxy() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_REFCOUNT(Vst3PlugViewProxy) +#pragma GCC diagnostic pop + +tresult PLUGIN_API Vst3PlugViewProxy::queryInterface(Steinberg::FIDString _iid, + void** obj) { + if (YaPlugView::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::FUnknown::iid, + Steinberg::IPlugView) + QUERY_INTERFACE(_iid, obj, Steinberg::IPlugView::iid, + Steinberg::IPlugView) + } + + *obj = nullptr; + return Steinberg::kNoInterface; +} diff --git a/src/common/serialization/vst3/plug-view-proxy.h b/src/common/serialization/vst3/plug-view-proxy.h new file mode 100644 index 00000000..4a04bc4c --- /dev/null +++ b/src/common/serialization/vst3/plug-view-proxy.h @@ -0,0 +1,110 @@ +// 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 . + +#pragma once + +#include "../common.h" +#include "plug-view/plug-view.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * An abstract class that implements `IPlugView`, and optionally also all + * extensions to `IPlugView` depending on what the plugin's implementation + * supports. This provides a proxy for the `IPlugView*` returned by a plugin on + * `IEditController::createView()`, and it works exactly the same as + * `Vst3PluginProxy`. + */ +class Vst3PlugViewProxy : public YaPlugView { + public: + /** + * These are the arguments for constructing a + * `Vst3PlugViewProxyImpl`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Read from an existing object. We will try to mimic this object, so + * we'll support any interfaces this object also supports. + */ + ConstructArgs(Steinberg::IPtr object, + size_t owner_instance_id); + + /** + * The unique instance identifier of the proxy object that returned this + * `IPlugView*`. This way we can refer to the correct 'actual' + * `IPlugView*` when the host calls a function on this object. + */ + native_size_t owner_instance_id; + + YaPlugView::ConstructArgs plug_view_args; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.object(plug_view_args); + } + }; + + /** + * Instantiate this instance with arguments read from an actual component + * handler. + * + * @note Since this is passed as part of `IEditController::createView()`, + * there are is no direct `Construct` + * message. The destructor should still send a message to drop the + * original smart pointer. + */ + Vst3PlugViewProxy(const ConstructArgs&& args); + + /** + * Message to request the Wine plugin host to destroy the `IPlugView*` + * returned by the object with the given instance ID. Sent from the + * destructor of `Vst3PlugViewProxyImpl`. + */ + struct Destruct { + using Response = Ack; + + native_size_t owner_instance_id; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + } + }; + + /** + * @remark The plugin side implementation should send a control message to + * clean up the instance on the Wine side in its destructor. + */ + virtual ~Vst3PlugViewProxy() = 0; + + DECLARE_FUNKNOWN_METHODS + + /** + * Get the instance ID of the owner of this object. + */ + inline size_t owner_instance_id() const { + return arguments.owner_instance_id; + } + + private: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/plug-view/plug-view.cpp b/src/common/serialization/vst3/plug-view/plug-view.cpp new file mode 100644 index 00000000..d5959681 --- /dev/null +++ b/src/common/serialization/vst3/plug-view/plug-view.cpp @@ -0,0 +1,26 @@ +// 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 . + +#include "plug-view.h" + +YaPlugView::ConstructArgs::ConstructArgs() {} + +YaPlugView::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported(Steinberg::FUnknownPtr(object)) {} + +YaPlugView::YaPlugView(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plug-view/plug-view.h b/src/common/serialization/vst3/plug-view/plug-view.h new file mode 100644 index 00000000..6ace7f7c --- /dev/null +++ b/src/common/serialization/vst3/plug-view/plug-view.h @@ -0,0 +1,358 @@ +// 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 . + +#pragma once + +#include + +#include "../../common.h" +#include "../base.h" +#include "../plug-frame-proxy.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IPlugView` for serialization purposes. This is instantiated as + * part of `Vst3PlugViewProxy`. + */ +class YaPlugView : public Steinberg::IPlugView { + public: + /** + * These are the arguments for creating a `YaPlugView`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements `IPlugView` and + * read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaPlugView(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to + * `IPlugView::isPlatformTypeSupported(type)` to the Wine plugin host. We + * will of course change `kPlatformStringLinux` for `kPlatformStringWin`, + * because why would a Windows VST3 plugin have X11 support? (and how would + * that even work) + */ + struct IsPlatformTypeSupported { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + std::string type; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.text1b(type, 128); + } + }; + + virtual tresult PLUGIN_API + isPlatformTypeSupported(Steinberg::FIDString type) override = 0; + + /** + * Message to pass through a call to `IPlugView::attached(parent, type)` to + * the Wine plugin host. Like mentioned above we will substitute + * `kPlatformStringWin` for `kPlatformStringLinux`. + */ + struct Attached { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + /** + * The parent handle passed by the host. This will be an + * `xcb_window_id`, and we'll embed the Wine window into it ourselves. + */ + native_size_t parent; + std::string type; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value8b(parent); + s.text1b(type, 128); + } + }; + + virtual tresult PLUGIN_API attached(void* parent, + Steinberg::FIDString type) override = 0; + + /** + * Message to pass through a call to `IPlugView::removed()` to the Wine + * plugin host. + */ + struct Removed { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + } + }; + + virtual tresult PLUGIN_API removed() override = 0; + + /** + * Message to pass through a call to `IPlugView::onWheel(distance)` to the + * Wine plugin host. + */ + struct OnWheel { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + float distance; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value4b(distance); + } + }; + + virtual tresult PLUGIN_API onWheel(float distance) override = 0; + + /** + * Message to pass through a call to `IPlugView::onKeyDown(key, keyCode, + * modifiers)` to the Wine plugin host. + */ + struct OnKeyDown { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + char16 key; + int16 key_code; + int16 modifiers; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value2b(key); + s.value2b(key_code); + s.value2b(modifiers); + } + }; + + virtual tresult PLUGIN_API onKeyDown(char16 key, + int16 keyCode, + int16 modifiers) override = 0; + + /** + * Message to pass through a call to `IPlugView::onKeyUp(key, keyCode, + * modifiers)` to the Wine plugin host. + */ + struct OnKeyUp { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + char16 key; + int16 key_code; + int16 modifiers; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value2b(key); + s.value2b(key_code); + s.value2b(modifiers); + } + }; + + virtual tresult PLUGIN_API onKeyUp(char16 key, + int16 keyCode, + int16 modifiers) override = 0; + + /** + * The response code and editor size returned by a call to + * `IPlugView::getSize(&size)`. + */ + struct GetSizeResponse { + UniversalTResult result; + Steinberg::ViewRect updated_size; + + template + void serialize(S& s) { + s.object(result); + s.object(updated_size); + } + }; + + /** + * Message to pass through a call to `IPlugView::getSize(&size)`. + */ + struct GetSize { + using Response = GetSizeResponse; + + native_size_t owner_instance_id; + + Steinberg::ViewRect size; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.object(size); + } + }; + + virtual tresult PLUGIN_API getSize(Steinberg::ViewRect* size) override = 0; + + /** + * Message to pass through a call to `IPlugView::onSize(new_size)` to the + * Wine plugin host. + */ + struct OnSize { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + Steinberg::ViewRect new_size; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.object(new_size); + } + }; + + virtual tresult PLUGIN_API + onSize(Steinberg::ViewRect* newSize) override = 0; + + /** + * Message to pass through a call to `IPlugView::onFocus(state)` to the Wine + * plugin host. + */ + struct OnFocus { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + TBool state; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.value1b(state); + } + }; + + virtual tresult PLUGIN_API onFocus(TBool state) override = 0; + + /** + * Message to pass through a call to `IPlugView::setFrame()` to the Wine + * plugin host. We will read what interfaces the passed `IPlugFrame` object + * implements so we can then create a proxy object on the Wine side that the + * plugin can use to make callbacks with. The lifetime of this + * `Vst3PlugFrameProxy` object should be bound to the `IPlugView` we are + * creating it for. + */ + struct SetFrame { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + Vst3PlugFrameProxy::ConstructArgs plug_frame_args; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.object(plug_frame_args); + } + }; + + virtual tresult PLUGIN_API + setFrame(Steinberg::IPlugFrame* frame) override = 0; + + /** + * Message to pass through a call to `IPlugView::canResize()` to the Wine + * plugin host. + */ + struct CanResize { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + } + }; + + virtual tresult PLUGIN_API canResize() override = 0; + + /** + * Message to pass through a call to `IPlugView::checkSizeConstraint(rect)` + * to the Wine plugin host. + */ + struct CheckSizeConstraint { + using Response = UniversalTResult; + + native_size_t owner_instance_id; + + Steinberg::ViewRect rect; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.object(rect); + } + }; + + virtual tresult PLUGIN_API + checkSizeConstraint(Steinberg::ViewRect* rect) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop + +namespace Steinberg { +template +void serialize(S& s, ViewRect& rect) { + s.value4b(rect.left); + s.value4b(rect.top); + s.value4b(rect.right); + s.value4b(rect.bottom); +} +} // namespace Steinberg diff --git a/src/common/serialization/vst3/plugin-factory.cpp b/src/common/serialization/vst3/plugin-factory.cpp new file mode 100644 index 00000000..3990e3a9 --- /dev/null +++ b/src/common/serialization/vst3/plugin-factory.cpp @@ -0,0 +1,162 @@ +// 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 . + +#include "plugin-factory.h" + +#include +#include + +#include + +YaPluginFactory::ConstructArgs::ConstructArgs() {} + +YaPluginFactory::ConstructArgs::ConstructArgs( + Steinberg::IPtr factory) { + // `IPluginFactory::getFactoryInfo` + if (Steinberg::PFactoryInfo info; + factory->getFactoryInfo(&info) == Steinberg::kResultOk) { + factory_info = info; + } + // `IPluginFactory::countClasses` + num_classes = factory->countClasses(); + // `IPluginFactory::getClassInfo` + class_infos_1.resize(num_classes); + for (int i = 0; i < num_classes; i++) { + Steinberg::PClassInfo info; + if (factory->getClassInfo(i, &info) == Steinberg::kResultOk) { + class_infos_1[i] = info; + } + } + + Steinberg::FUnknownPtr factory2(factory); + if (!factory2) { + return; + } + + supports_plugin_factory_2 = true; + // `IpluginFactory2::getClassInfo2` + class_infos_2.resize(num_classes); + for (int i = 0; i < num_classes; i++) { + Steinberg::PClassInfo2 info; + if (factory2->getClassInfo2(i, &info) == Steinberg::kResultOk) { + class_infos_2[i] = info; + } + } + + Steinberg::FUnknownPtr factory3(factory); + if (!factory3) { + return; + } + + supports_plugin_factory_3 = true; + // `IpluginFactory3::getClassInfoUnicode` + class_infos_unicode.resize(num_classes); + for (int i = 0; i < num_classes; i++) { + Steinberg::PClassInfoW info; + if (factory3->getClassInfoUnicode(i, &info) == Steinberg::kResultOk) { + class_infos_unicode[i] = info; + } + } +} + +YaPluginFactory::YaPluginFactory(const ConstructArgs&& args) + : arguments(std::move(args)){FUNKNOWN_CTOR} + + // clang-format just doesn't understand these macros, I guess + YaPluginFactory::~YaPluginFactory() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_REFCOUNT(YaPluginFactory) +#pragma GCC diagnostic pop + +tresult PLUGIN_API YaPluginFactory::queryInterface(Steinberg::FIDString _iid, + void** obj) { + QUERY_INTERFACE(_iid, obj, Steinberg::FUnknown::iid, + Steinberg::IPluginFactory) + QUERY_INTERFACE(_iid, obj, Steinberg::IPluginFactory::iid, + Steinberg::IPluginFactory) + if (arguments.supports_plugin_factory_2) { + QUERY_INTERFACE(_iid, obj, Steinberg::IPluginFactory2::iid, + Steinberg::IPluginFactory2) + } + if (arguments.supports_plugin_factory_3) { + QUERY_INTERFACE(_iid, obj, Steinberg::IPluginFactory3::iid, + Steinberg::IPluginFactory3) + } + + *obj = nullptr; + return Steinberg::kNoInterface; +} + +tresult PLUGIN_API +YaPluginFactory::getFactoryInfo(Steinberg::PFactoryInfo* info) { + if (info && arguments.factory_info) { + *info = *arguments.factory_info; + return Steinberg::kResultOk; + } else { + return Steinberg::kNotInitialized; + } +} + +int32 PLUGIN_API YaPluginFactory::countClasses() { + return arguments.num_classes; +} + +tresult PLUGIN_API YaPluginFactory::getClassInfo(Steinberg::int32 index, + Steinberg::PClassInfo* info) { + if (index >= static_cast(arguments.class_infos_1.size())) { + return Steinberg::kInvalidArgument; + } + + if (arguments.class_infos_1[index]) { + *info = *arguments.class_infos_1[index]; + return Steinberg::kResultOk; + } else { + return Steinberg::kResultFalse; + } +} + +tresult PLUGIN_API +YaPluginFactory::getClassInfo2(int32 index, Steinberg::PClassInfo2* info) { + if (index >= static_cast(arguments.class_infos_2.size())) { + return Steinberg::kInvalidArgument; + } + + if (arguments.class_infos_2[index]) { + *info = *arguments.class_infos_2[index]; + return Steinberg::kResultOk; + } else { + return Steinberg::kResultFalse; + } +} + +tresult PLUGIN_API +YaPluginFactory::getClassInfoUnicode(int32 index, + Steinberg::PClassInfoW* info) { + if (index >= static_cast(arguments.class_infos_unicode.size())) { + return Steinberg::kInvalidArgument; + } + + if (arguments.class_infos_unicode[index]) { + *info = *arguments.class_infos_unicode[index]; + return Steinberg::kResultOk; + } else { + return Steinberg::kResultFalse; + } +} diff --git a/src/common/serialization/vst3/plugin-factory.h b/src/common/serialization/vst3/plugin-factory.h new file mode 100644 index 00000000..13292762 --- /dev/null +++ b/src/common/serialization/vst3/plugin-factory.h @@ -0,0 +1,226 @@ +// 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 . + +#pragma once + +#include +#include +#include + +#include "../../bitsery/ext/vst3.h" +#include "base.h" +#include "host-context-proxy.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IPluginFactory{1,2,3}` for serialization purposes. See + * `docs/vst3.md` for more information on how this works. + */ +class YaPluginFactory : public Steinberg::IPluginFactory3 { + public: + /** + * These are the arguments for constructing a `YaPluginFactoryImpl`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Create a copy of an existing plugin factory. Depending on the + * supported interface function more or less of this struct will be left + * empty, and `known_iids` will be set accordingly. + */ + ConstructArgs(Steinberg::IPtr factory); + + /** + * Whether `factory` supported `IPluginFactory2`. + */ + bool supports_plugin_factory_2 = false; + + /** + * Whether `factory` supported `IPluginFactory3`. + */ + bool supports_plugin_factory_3 = false; + + /** + * For `IPluginFactory::getFactoryInfo`. + */ + std::optional factory_info; + + /** + * For `IPluginFactory::countClasses`. + */ + int num_classes; + + /** + * For `IPluginFactory::getClassInfo`. We need to store all four class + * info versions if the plugin can provide them since we don't know + * which version of the interface the host will use. Will be + * `std::nullopt` if the plugin doesn't return a class info. + */ + std::vector> class_infos_1; + + /** + * For `IPluginFactory2::getClassInfo2`, works the same way as the + * above. + */ + std::vector> class_infos_2; + + /** + * For `IPluginFactory3::getClassInfoUnicode`, works the same way as the + * above. + */ + std::vector> class_infos_unicode; + + template + void serialize(S& s) { + s.value1b(supports_plugin_factory_2); + s.value1b(supports_plugin_factory_3); + s.ext(factory_info, bitsery::ext::StdOptional{}); + s.value4b(num_classes); + s.container(class_infos_1, 2048, + [](S& s, std::optional& info) { + s.ext(info, bitsery::ext::StdOptional{}); + }); + s.container(class_infos_2, 2048, + [](S& s, std::optional& info) { + s.ext(info, bitsery::ext::StdOptional{}); + }); + s.container(class_infos_unicode, 2048, + [](S& s, std::optional& info) { + s.ext(info, bitsery::ext::StdOptional{}); + }); + } + }; + + /** + * Message to request the `IPluginFactory{,2,3}`'s information from the Wine + * plugin host. + */ + struct Construct { + using Response = ConstructArgs; + + template + void serialize(S&) {} + }; + + /** + * Instantiate this instance with arguments read from the Windows VST3 + * plugin's plugin factory. + */ + YaPluginFactory(const ConstructArgs&& args); + + /** + * We do not need to implement the destructor in `YaPluginFactoryImpl`, + * since when the sockets are closed, RAII will clean up the Windows VST3 + * module we loaded along with its factory for us. + */ + virtual ~YaPluginFactory(); + + DECLARE_FUNKNOWN_METHODS + + // From `IPluginFactory` + tresult PLUGIN_API getFactoryInfo(Steinberg::PFactoryInfo* info) override; + int32 PLUGIN_API countClasses() override; + tresult PLUGIN_API getClassInfo(Steinberg::int32 index, + Steinberg::PClassInfo* info) override; + /** + * See the implementation in `YaPluginFactoryImpl` for how this is handled. + */ + virtual tresult PLUGIN_API createInstance(Steinberg::FIDString cid, + Steinberg::FIDString _iid, + void** obj) override = 0; + + // From `IPluginFactory2` + tresult PLUGIN_API getClassInfo2(int32 index, + Steinberg::PClassInfo2* info) override; + + // From `IPluginFactory3` + tresult PLUGIN_API + getClassInfoUnicode(int32 index, Steinberg::PClassInfoW* info) override; + + /** + * Message to pass through a call to `IPluginFactory3::setHostContext()` to + * the Wine plugin host. A `Vst3HostContextProxy` should be created on the + * Wine plugin host and then passed as an argument to + * `IPluginFactory3::setHostContext()`. + */ + struct SetHostContext { + using Response = UniversalTResult; + + Vst3HostContextProxy::ConstructArgs host_context_args; + + template + void serialize(S& s) { + s.object(host_context_args); + } + }; + + virtual tresult PLUGIN_API + setHostContext(Steinberg::FUnknown* context) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop + +// Serialization functions have to live in the same namespace as the objects +// they're serializing +namespace Steinberg { +template +void serialize(S& s, PClassInfo& class_info) { + s.container1b(class_info.cid); + s.value4b(class_info.cardinality); + s.text1b(class_info.category); + s.text1b(class_info.name); +} + +template +void serialize(S& s, PClassInfo2& class_info) { + s.container1b(class_info.cid); + s.value4b(class_info.cardinality); + s.text1b(class_info.category); + s.text1b(class_info.name); + s.value4b(class_info.classFlags); + s.text1b(class_info.subCategories); + s.text1b(class_info.vendor); + s.text1b(class_info.version); + s.text1b(class_info.sdkVersion); +} + +template +void serialize(S& s, PClassInfoW& class_info) { + s.container1b(class_info.cid); + s.value4b(class_info.cardinality); + s.text1b(class_info.category); + s.text2b(class_info.name); + s.value4b(class_info.classFlags); + s.text1b(class_info.subCategories); + s.text2b(class_info.vendor); + s.text2b(class_info.version); + s.text2b(class_info.sdkVersion); +} + +template +void serialize(S& s, PFactoryInfo& factory_info) { + s.text1b(factory_info.vendor); + s.text1b(factory_info.url); + s.text1b(factory_info.email); + s.value4b(factory_info.flags); +} +} // namespace Steinberg diff --git a/src/common/serialization/vst3/plugin-proxy.cpp b/src/common/serialization/vst3/plugin-proxy.cpp new file mode 100644 index 00000000..3ec835b6 --- /dev/null +++ b/src/common/serialization/vst3/plugin-proxy.cpp @@ -0,0 +1,111 @@ +// 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 . + +#include "plugin-proxy.h" + +Vst3PluginProxy::ConstructArgs::ConstructArgs() {} + +Vst3PluginProxy::ConstructArgs::ConstructArgs( + Steinberg::IPtr object, + size_t instance_id) + : instance_id(instance_id), + audio_processor_args(object), + component_args(object), + connection_point_args(object), + edit_controller_args(object), + edit_controller_2_args(object), + plugin_base_args(object), + program_list_data_args(object), + unit_data_args(object), + unit_info_args(object) {} + +Vst3PluginProxy::Vst3PluginProxy(const ConstructArgs&& args) + : YaAudioProcessor(std::move(args.audio_processor_args)), + YaComponent(std::move(args.component_args)), + YaConnectionPoint(std::move(args.connection_point_args)), + YaEditController(std::move(args.edit_controller_args)), + YaEditController2(std::move(args.edit_controller_2_args)), + YaPluginBase(std::move(args.plugin_base_args)), + YaProgramListData(std::move(args.program_list_data_args)), + YaUnitData(std::move(args.unit_data_args)), + YaUnitInfo(std::move(args.unit_info_args)), + arguments(std::move(args)){FUNKNOWN_CTOR} + + Vst3PluginProxy::~Vst3PluginProxy() { + FUNKNOWN_DTOR +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" +IMPLEMENT_REFCOUNT(Vst3PluginProxy) +#pragma GCC diagnostic pop + +tresult PLUGIN_API Vst3PluginProxy::queryInterface(Steinberg::FIDString _iid, + void** obj) { + if (YaPluginBase::supported()) { + // We had to expand the macro here because we need to cast through + // `YaPluginBase`, since `IpluginBase` is also a base of `IComponent` + if (Steinberg::FUnknownPrivate ::iidEqual(_iid, + Steinberg::FUnknown::iid)) { + addRef(); + *obj = static_cast( + static_cast(this)); + return ::Steinberg ::kResultOk; + } + if (Steinberg::FUnknownPrivate ::iidEqual( + _iid, Steinberg::IPluginBase::iid)) { + addRef(); + *obj = static_cast( + static_cast(this)); + return ::Steinberg ::kResultOk; + } + } + if (YaAudioProcessor::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IAudioProcessor::iid, + Steinberg::Vst::IAudioProcessor) + } + if (YaComponent::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IComponent::iid, + Steinberg::Vst::IComponent) + } + if (YaConnectionPoint::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IConnectionPoint::iid, + Steinberg::Vst::IConnectionPoint) + } + if (YaEditController::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IEditController::iid, + Steinberg::Vst::IEditController) + } + if (YaEditController2::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IEditController2::iid, + Steinberg::Vst::IEditController2) + } + if (YaProgramListData::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IProgramListData::iid, + Steinberg::Vst::IProgramListData) + } + if (YaUnitData::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IUnitData::iid, + Steinberg::Vst::IUnitData) + } + if (YaUnitInfo::supported()) { + QUERY_INTERFACE(_iid, obj, Steinberg::Vst::IUnitInfo::iid, + Steinberg::Vst::IUnitInfo) + } + + *obj = nullptr; + return Steinberg::kNoInterface; +} diff --git a/src/common/serialization/vst3/plugin-proxy.h b/src/common/serialization/vst3/plugin-proxy.h new file mode 100644 index 00000000..625cb1b0 --- /dev/null +++ b/src/common/serialization/vst3/plugin-proxy.h @@ -0,0 +1,239 @@ +// 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 . + +#pragma once + +#include + +#include "../common.h" +#include "plugin/audio-processor.h" +#include "plugin/component.h" +#include "plugin/connection-point.h" +#include "plugin/edit-controller-2.h" +#include "plugin/edit-controller.h" +#include "plugin/plugin-base.h" +#include "plugin/program-list-data.h" +#include "plugin/unit-data.h" +#include "plugin/unit-info.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * An abstract class that optionally implements all VST3 interfaces a plugin + * object could implement. A more in depth explanation can be found in + * `docs/vst3.md`, but the way this works is that we begin with an `FUnknown` + * pointer from the Windows VST3 plugin obtained by a call to + * `IPluginFactory::createInstance()` (with an interface decided by the host). + * We then go through all the plugin interfaces and check whether that object + * supports them one by one. For each supported interface we remember that the + * plugin supports it, and we'll optionally write down some static data (such as + * the edit controller cid) that can't change over the lifetime of the + * application. On the plugin side we then return a `Vst3PluginProxyImpl` object + * that contains all of this information about interfaces the object we're + * proxying might support. This way we can allow casts to all of those object + * types in `queryInterface()`, essentially perfectly mimicing the original + * object. + * + * This monolith approach is also important when it comes to `IConnectionPoint`. + * The host should be able to connect arbitrary objects together, and the plugin + * can then use the query interface smart pointer casting system to cast those + * objects to the types they want. By having a huge monolithic class that + * implements any interface such an object might also implement, we can allow + * perfect proxying behaviour for connecting components. + */ +class Vst3PluginProxy : public YaAudioProcessor, + public YaComponent, + public YaConnectionPoint, + public YaEditController, + public YaEditController2, + public YaPluginBase, + public YaProgramListData, + public YaUnitData, + public YaUnitInfo { + public: + /** + * These are the arguments for constructing a `Vst3PluginProxyImpl`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Read from an existing object. We will try to mimic this object, so + * we'll support any interfaces this object also supports. + */ + ConstructArgs(Steinberg::IPtr object, size_t instance_id); + + /** + * The unique identifier for this specific object instance. + */ + native_size_t instance_id; + + YaAudioProcessor::ConstructArgs audio_processor_args; + YaComponent::ConstructArgs component_args; + YaConnectionPoint::ConstructArgs connection_point_args; + YaEditController::ConstructArgs edit_controller_args; + YaEditController2::ConstructArgs edit_controller_2_args; + YaPluginBase::ConstructArgs plugin_base_args; + YaProgramListData::ConstructArgs program_list_data_args; + YaUnitData::ConstructArgs unit_data_args; + YaUnitInfo::ConstructArgs unit_info_args; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.object(audio_processor_args); + s.object(component_args); + s.object(connection_point_args); + s.object(edit_controller_args); + s.object(edit_controller_2_args); + s.object(plugin_base_args); + s.object(program_list_data_args); + s.object(unit_data_args); + s.object(unit_info_args); + } + }; + + /** + * Message to request the Wine plugin host to instantiate a new IComponent + * to pass through a call to `IComponent::createInstance(cid, + * ::iid, ...)`. + */ + struct Construct { + using Response = std::variant; + + ArrayUID cid; + + /** + * The interface the host was trying to instantiate an object for. + * Technically the host can create any kind of object, but these are the + * objects that are actually used. + */ + enum class Interface { + IComponent, + IEditController, + }; + + Interface requested_interface; + + template + void serialize(S& s) { + s.container1b(cid); + s.value4b(requested_interface); + } + }; + + /** + * Instantiate this object instance with arguments read from another + * interface implementation. + */ + Vst3PluginProxy(const ConstructArgs&& args); + + /** + * Message to request the Wine plugin host to destroy this object instance + * with the given instance ID. Sent from the destructor of + * `Vst3PluginProxyImpl`. This will cause all smart pointers to the actual + * object in the Wine plugin host to be dropped. + */ + struct Destruct { + using Response = Ack; + + native_size_t instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + } + }; + + /** + * @remark The plugin side implementation should send a control message to + * clean up the instance on the Wine side in its destructor. + */ + virtual ~Vst3PluginProxy() = 0; + + DECLARE_FUNKNOWN_METHODS + + /** + * Get this object's instance ID. Used in `IConnectionPoint` to identify and + * connect specific objects. + */ + inline size_t instance_id() const { return arguments.instance_id; } + + // We'll define messages for functions that have identical definitions in + // multiple interfaces below. When the Wine plugin host process handles + // these it should check which of the interfaces is supported on the host. + + /** + * Message to pass through a call to + * `{IComponent,IEditController}::setState(state)` to the Wine plugin host. + */ + struct SetState { + using Response = UniversalTResult; + + native_size_t instance_id; + + VectorStream state; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.object(state); + } + }; + + /** + * The response code and written state for a call to + * `{IComponent,IEditController}::getState(state)`. + */ + struct GetStateResponse { + UniversalTResult result; + VectorStream updated_state; + + template + void serialize(S& s) { + s.object(result); + s.object(updated_state); + } + }; + + /** + * Message to pass through a call to + * `{IComponent,IEditController}::getState(state)` to the Wine plugin host. + */ + struct GetState { + using Response = GetStateResponse; + + native_size_t instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + } + }; + + private: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop + +template +void serialize( + S& s, + std::variant& result) { + s.ext(result, bitsery::ext::StdVariant{}); +} diff --git a/src/common/serialization/vst3/plugin/audio-processor.cpp b/src/common/serialization/vst3/plugin/audio-processor.cpp new file mode 100644 index 00000000..15e7f99d --- /dev/null +++ b/src/common/serialization/vst3/plugin/audio-processor.cpp @@ -0,0 +1,27 @@ +// 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 . + +#include "audio-processor.h" + +YaAudioProcessor::ConstructArgs::ConstructArgs() {} + +YaAudioProcessor::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported( + Steinberg::FUnknownPtr(object)) {} + +YaAudioProcessor::YaAudioProcessor(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plugin/audio-processor.h b/src/common/serialization/vst3/plugin/audio-processor.h new file mode 100644 index 00000000..3e3c28a7 --- /dev/null +++ b/src/common/serialization/vst3/plugin/audio-processor.h @@ -0,0 +1,313 @@ +// 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 . + +#pragma once + +#include +#include + +#include "../../common.h" +#include "../base.h" +#include "../process-data.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IAudioProcessor` for serialization purposes. This is + * instantiated as part of `Vst3PluginProxy`. + */ +class YaAudioProcessor : public Steinberg::Vst::IAudioProcessor { + public: + /** + * These are the arguments for creating a `YaAudioProcessor`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements `IAudioProcessor` + * and read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaAudioProcessor(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to + * `IAudioProcessor::setBusArrangements(inputs, num_ins, outputs, num_outs)` + * to the Wine plugin host. + */ + struct SetBusArrangements { + using Response = UniversalTResult; + + native_size_t instance_id; + + // These are orginally C-style heap arrays, not normal pointers + std::vector inputs; + int32 num_ins; + std::vector outputs; + int32 num_outs; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.container8b(inputs, max_num_speakers); + s.value4b(num_ins); + s.container8b(outputs, max_num_speakers); + s.value4b(num_outs); + } + }; + + virtual tresult PLUGIN_API + setBusArrangements(Steinberg::Vst::SpeakerArrangement* inputs, + int32 numIns, + Steinberg::Vst::SpeakerArrangement* outputs, + int32 numOuts) override = 0; + + /** + * The response code and written state for a call to + * `IAudioProcessor::getBusArrangement(dir, index, arr)`. + */ + struct GetBusArrangementResponse { + UniversalTResult result; + Steinberg::Vst::SpeakerArrangement updated_arr; + + template + void serialize(S& s) { + s.object(result); + s.value8b(updated_arr); + } + }; + + /** + * Message to pass through a call to + * `IAudioProcessor::getBusArrangement(dir, index, arr)` to the Wine + * plugin host. + */ + struct GetBusArrangement { + using Response = GetBusArrangementResponse; + + native_size_t instance_id; + + Steinberg::Vst::BusDirection dir; + int32 index; + Steinberg::Vst::SpeakerArrangement arr; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(dir); + s.value4b(index); + s.value8b(arr); + } + }; + + virtual tresult PLUGIN_API + getBusArrangement(Steinberg::Vst::BusDirection dir, + int32 index, + Steinberg::Vst::SpeakerArrangement& arr) override = 0; + + /** + * Message to pass through a call to + * `IAudioProcessor::canProcessSampleSize(symbolic_sample_size)` to the Wine + * plugin host. + */ + struct CanProcessSampleSize { + using Response = UniversalTResult; + + native_size_t instance_id; + + int32 symbolic_sample_size; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(symbolic_sample_size); + } + }; + + virtual tresult PLUGIN_API + canProcessSampleSize(int32 symbolicSampleSize) override = 0; + + /** + * Message to pass through a call to `IAudioProcessor::getLatencySamples()` + * to the Wine plugin host. + */ + struct GetLatencySamples { + using Response = PrimitiveWrapper; + + native_size_t instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + } + }; + + virtual uint32 PLUGIN_API getLatencySamples() override = 0; + + /** + * Message to pass through a call to + * `IAudioProcessor::setupProcessing(setup)` to the Wine plugin host. + */ + struct SetupProcessing { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::ProcessSetup setup; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.object(setup); + } + }; + + virtual tresult PLUGIN_API + setupProcessing(Steinberg::Vst::ProcessSetup& setup) override = 0; + + /** + * Message to pass through a call to `IAudioProcessor::setProcessing(state)` + * to the Wine plugin host. + */ + struct SetProcessing { + using Response = UniversalTResult; + + native_size_t instance_id; + + TBool state; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value1b(state); + } + }; + + virtual tresult PLUGIN_API setProcessing(TBool state) override = 0; + + /** + * The response code and all the output data resulting from a call to + * `IAudioProcessor::process(data)`. + */ + struct ProcessResponse { + UniversalTResult result; + YaProcessDataResponse output_data; + + template + void serialize(S& s) { + s.object(result); + s.object(output_data); + } + }; + + /** + * Message to pass through a call to `IAudioProcessor::process(data)` to the + * Wine plugin host. This `YaProcessData` object wraps around all input + * audio buffers, parameter changes and events along with all context data + * provided by the host so we can send it to the Wine plugin host. We can + * then use `YaProcessData::get()` on the Wine plugin host side to + * reconstruct the original `ProcessData` object, and we then finally use + * `YaProcessData::move_outputs_to_response()` to create a response object + * that we can write back to the `ProcessData` object provided by the host. + */ + struct Process { + using Response = ProcessResponse; + + native_size_t instance_id; + + YaProcessData data; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.object(data); + } + }; + + virtual tresult PLUGIN_API + process(Steinberg::Vst::ProcessData& data) override = 0; + + /** + * Message to pass through a call to `IAudioProcessor::getTailSamples()` + * to the Wine plugin host. + */ + struct GetTailSamples { + using Response = PrimitiveWrapper; + + native_size_t instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + } + }; + + virtual uint32 PLUGIN_API getTailSamples() override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop + +namespace Steinberg { +namespace Vst { +template +void serialize(S& s, Steinberg::Vst::BusInfo& info) { + s.value4b(info.mediaType); + s.value4b(info.direction); + s.value4b(info.channelCount); + s.container2b(info.name); + s.value4b(info.busType); + s.value4b(info.flags); +} + +template +void serialize(S& s, Steinberg::Vst::RoutingInfo& info) { + s.value4b(info.mediaType); + s.value4b(info.busIndex); + s.value4b(info.channel); +} + +template +void serialize(S& s, Steinberg::Vst::ProcessSetup& setup) { + s.value4b(setup.processMode); + s.value4b(setup.symbolicSampleSize); + s.value4b(setup.maxSamplesPerBlock); + s.value8b(setup.sampleRate); +} +} // namespace Vst +} // namespace Steinberg diff --git a/src/common/serialization/vst3/plugin/component.cpp b/src/common/serialization/vst3/plugin/component.cpp new file mode 100644 index 00000000..b66d12fd --- /dev/null +++ b/src/common/serialization/vst3/plugin/component.cpp @@ -0,0 +1,26 @@ +// 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 . + +#include "component.h" + +YaComponent::ConstructArgs::ConstructArgs() {} + +YaComponent::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported(Steinberg::FUnknownPtr(object)) {} + +YaComponent::YaComponent(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plugin/component.h b/src/common/serialization/vst3/plugin/component.h new file mode 100644 index 00000000..7ca7f0db --- /dev/null +++ b/src/common/serialization/vst3/plugin/component.h @@ -0,0 +1,297 @@ +// 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 . + +#pragma once + +#include +#include +#include + +#include "../../../bitsery/ext/vst3.h" +#include "../../common.h" +#include "../base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IComponent` for serialization purposes. This is instantiated as + * part of `Vst3PluginProxy`. Event though `IComponent` inherits from + * `IPlguinBase`, we'll implement that separately in `YaPluginBase` because + * `IEditController` also inherits from `IPluginBase`. + * + * TODO: Remove the original fields for out parameters in the structs. They're + * really supposed to be empty. + */ +class YaComponent : public Steinberg::Vst::IComponent { + public: + /** + * These are the arguments for creating a `YaComponent`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements `IComponent` and + * read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaComponent(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * The response code and returned CID for a call to + * `IComponent::getControllerClassId()`. + */ + struct GetControllerClassIdResponse { + UniversalTResult result; + ArrayUID editor_cid; + + template + void serialize(S& s) { + s.object(result); + s.container1b(editor_cid); + } + }; + + /** + * Message to pass through a call to `IComponent::getControllerClassId()` to + * the Wine plugin host. + */ + struct GetControllerClassId { + using Response = GetControllerClassIdResponse; + + native_size_t instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + } + }; + + virtual tresult PLUGIN_API + getControllerClassId(Steinberg::TUID classId) override = 0; + + /** + * Message to pass through a call to `IComponent::setIoMode(mode)` to the + * Wine plugin host. + */ + struct SetIoMode { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::IoMode mode; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(mode); + } + }; + + virtual tresult PLUGIN_API + setIoMode(Steinberg::Vst::IoMode mode) override = 0; + + /** + * Message to pass through a call to `IComponent::getBusCount(type, dir)` to + * the Wine plugin host. + */ + struct GetBusCount { + using Response = PrimitiveWrapper; + + native_size_t instance_id; + + Steinberg::Vst::BusType type; + Steinberg::Vst::BusDirection dir; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(type); + s.value4b(dir); + } + }; + + virtual int32 PLUGIN_API + getBusCount(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir) override = 0; + + /** + * The response code and returned bus information for a call to + * `IComponent::getBusInfo(type, dir, index, bus )`. + */ + struct GetBusInfoResponse { + UniversalTResult result; + Steinberg::Vst::BusInfo updated_bus; + + template + void serialize(S& s) { + s.object(result); + s.object(updated_bus); + } + }; + + /** + * Message to pass through a call to `IComponent::getBusInfo(type, dir, + * index, bus )` to the Wine plugin host. + */ + struct GetBusInfo { + using Response = GetBusInfoResponse; + + native_size_t instance_id; + + Steinberg::Vst::BusType type; + Steinberg::Vst::BusDirection dir; + int32 index; + Steinberg::Vst::BusInfo bus; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(type); + s.value4b(dir); + s.object(bus); + } + }; + + virtual tresult PLUGIN_API + getBusInfo(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir, + int32 index, + Steinberg::Vst::BusInfo& bus /*out*/) override = 0; + + /** + * The response code and returned routing information for a call to + * `IComponent::getRoutingInfo(in_info, out_info )`. + */ + struct GetRoutingInfoResponse { + UniversalTResult result; + Steinberg::Vst::RoutingInfo updated_in_info; + Steinberg::Vst::RoutingInfo updated_out_info; + + template + void serialize(S& s) { + s.object(result); + s.object(updated_in_info); + s.object(updated_out_info); + } + }; + + /** + * Message to pass through a call to `IComponent::getRoutingInfo(in_info, + * out_info )` to the Wine plugin host. + */ + struct GetRoutingInfo { + using Response = GetRoutingInfoResponse; + + native_size_t instance_id; + + Steinberg::Vst::RoutingInfo in_info; + Steinberg::Vst::RoutingInfo out_info; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.object(in_info); + s.object(out_info); + } + }; + + virtual tresult PLUGIN_API + getRoutingInfo(Steinberg::Vst::RoutingInfo& inInfo, + Steinberg::Vst::RoutingInfo& outInfo /*out*/) override = 0; + + /** + * Message to pass through a call to `IComponent::activateBus(type, dir, + * index, state)` to the Wine plugin host. + */ + struct ActivateBus { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::MediaType type; + Steinberg::Vst::BusDirection dir; + int32 index; + TBool state; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(type); + s.value4b(dir); + s.value4b(index); + s.value1b(state); + } + }; + + virtual tresult PLUGIN_API activateBus(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir, + int32 index, + TBool state) override = 0; + + /** + * Message to pass through a call to `IComponent::setActive(state)` to the + * Wine plugin host. + */ + struct SetActive { + using Response = UniversalTResult; + + native_size_t instance_id; + + TBool state; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value1b(state); + } + }; + + virtual tresult PLUGIN_API setActive(TBool state) override = 0; + + // `setState()` and `getState()` are defiend in both `IComponent` and + // `IEditController`. Since an object can only ever implement one or the + // other, the messages for calling either are defined directly on + // `Vst3PluginProxy`. + virtual tresult PLUGIN_API + setState(Steinberg::IBStream* state) override = 0; + virtual tresult PLUGIN_API + getState(Steinberg::IBStream* state) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/plugin/connection-point.cpp b/src/common/serialization/vst3/plugin/connection-point.cpp new file mode 100644 index 00000000..c0c6d443 --- /dev/null +++ b/src/common/serialization/vst3/plugin/connection-point.cpp @@ -0,0 +1,36 @@ +// 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 . + +#include "connection-point.h" + +YaConnectionPoint::ConstructArgs::ConstructArgs() {} + +YaConnectionPoint::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported( + Steinberg::FUnknownPtr(object)) {} + +YaConnectionPoint::Vst3ConnectionPointProxyConstructArgs:: + Vst3ConnectionPointProxyConstructArgs() {} + +YaConnectionPoint::Vst3ConnectionPointProxyConstructArgs:: + Vst3ConnectionPointProxyConstructArgs( + Steinberg::IPtr object, + size_t owner_instance_id) + : owner_instance_id(owner_instance_id), connection_point_args(object) {} + +YaConnectionPoint::YaConnectionPoint(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plugin/connection-point.h b/src/common/serialization/vst3/plugin/connection-point.h new file mode 100644 index 00000000..4dab808d --- /dev/null +++ b/src/common/serialization/vst3/plugin/connection-point.h @@ -0,0 +1,209 @@ +// 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 . + +#pragma once + +#include + +#include +#include +#include + +#include "../../common.h" +#include "../base.h" +#include "../message.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IConnectionPoint` for serialization purposes. This is + * instantiated as part of `Vst3PluginProxy`. Because we use this giant + * monolithic proxy class we can easily directly connect different objects by + * checking if they're a `Vst3PluginProxy` and then fetching that object's + * instance ID (if the host doesn't place a proxy object here). + */ +class YaConnectionPoint : public Steinberg::Vst::IConnectionPoint { + public: + /** + * These are the arguments for creating a `YaConnectionPoint`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements + * `IConnectionPoint` and read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + protected: + /** + * These are the arguments for constructing a + * `Vst3ConnectionPointProxyImpl`. + * + * It's defined here to work around circular includes. + */ + struct Vst3ConnectionPointProxyConstructArgs { + Vst3ConnectionPointProxyConstructArgs(); + + /** + * Read from an existing object. We will try to mimic this object, so + * we'll support any interfaces this object also supports. + * + * This is not necessary in this case since the object has to support + * `IConnectionPoint`, but let's stay consistent with the overall style + * here. + */ + Vst3ConnectionPointProxyConstructArgs(Steinberg::IPtr object, + size_t owner_instance_id); + + /** + * The unique instance identifier of the proxy object instance this + * connection proxy has been passed to and thus belongs to. This way we + * can refer to the correct 'actual' `IConnectionPoint` instance when + * the plugin calls `notify()` on this proxy object. + */ + native_size_t owner_instance_id; + + ConstructArgs connection_point_args; + + template + void serialize(S& s) { + s.value8b(owner_instance_id); + s.object(connection_point_args); + } + }; + + public: + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaConnectionPoint(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to `IConnectionPoint::connect(other)` to + * the Wine plugin host. If the host directly connects two objects, then + * we'll connect them directly as well. Otherwise all messages have to be + * routed through the host. + */ + struct Connect { + using Response = UniversalTResult; + + native_size_t instance_id; + + /** + * The other object this object should be connected to. When connecting + * two `Vst3PluginProxy` objects, we can directly connect the underlying + * objects on the Wine side using their instance IDs. Otherwise we'll + * create a proxy object for the connection proxy provided by the host + * that the plugin can use to send messages to. + */ + std::variant + other; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.ext(other, + bitsery::ext::StdVariant{ + [](S& s, native_size_t& other_instance_id) { + s.value8b(other_instance_id); + }, + [](S& s, Vst3ConnectionPointProxyConstructArgs& args) { + s.object(args); + }}); + } + }; + + virtual tresult PLUGIN_API connect(IConnectionPoint* other) override = 0; + + /** + * Message to pass through a call to `IConnectionPoint::disconnect(other)` + * to the Wine plugin host. + */ + struct Disconnect { + using Response = UniversalTResult; + + native_size_t instance_id; + + /** + * If we connected two objects directly, then this is the instance ID of + * that object. Otherwise we'll just destroy the smart pointer pointing + * to our `IConnectionPoint` proxy object. + */ + std::optional other_instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.ext(other_instance_id, bitsery::ext::StdOptional{}, + [](S& s, native_size_t& instance_id) { + s.value8b(instance_id); + }); + } + }; + + virtual tresult PLUGIN_API disconnect(IConnectionPoint* other) override = 0; + + /** + * Message to pass through a call to `IConnectionPoint::notify(message)` to + * the Wine plugin host. Since `IAttributeList` does not have any way to + * iterate over all values, we only support messages sent by plugins using + * our own implementation of the interface, since there's no way to + * serialize them otherwise. Additionally, plugins may store the `IMessage` + * pointer for later usage, so we have to pass through a pointer to the + * original message so it has the same lifetime as the original message. + * This `IConnectionPoint::notify()` implementation is also only used with + * hosts that do not connect objects directly and use connection proxies + * instead. + */ + struct Notify { + using Response = UniversalTResult; + + native_size_t instance_id; + + YaMessagePtr message_ptr; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.object(message_ptr); + } + }; + + virtual tresult PLUGIN_API + notify(Steinberg::Vst::IMessage* message) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/plugin/edit-controller-2.cpp b/src/common/serialization/vst3/plugin/edit-controller-2.cpp new file mode 100644 index 00000000..aef36d0a --- /dev/null +++ b/src/common/serialization/vst3/plugin/edit-controller-2.cpp @@ -0,0 +1,27 @@ +// 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 . + +#include "edit-controller-2.h" + +YaEditController2::ConstructArgs::ConstructArgs() {} + +YaEditController2::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported( + Steinberg::FUnknownPtr(object)) {} + +YaEditController2::YaEditController2(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plugin/edit-controller-2.h b/src/common/serialization/vst3/plugin/edit-controller-2.h new file mode 100644 index 00000000..f94e8682 --- /dev/null +++ b/src/common/serialization/vst3/plugin/edit-controller-2.h @@ -0,0 +1,129 @@ +// 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 . + +#pragma once + +#include + +#include "../../common.h" +#include "../base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IEditController2` for serialization purposes. This is + * instantiated as part of `Vst3PluginProxy`. + */ +class YaEditController2 : public Steinberg::Vst::IEditController2 { + public: + /** + * These are the arguments for creating a `YaEditController2`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements + * `IEditController2` and read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaEditController2(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to `IEditController2::setKnobMode(mode)` + * to the Wine plugin host. + */ + struct SetKnobMode { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::KnobMode mode; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(mode); + } + }; + + virtual tresult PLUGIN_API + setKnobMode(Steinberg::Vst::KnobMode mode) override = 0; + + /** + * Message to pass through a call to + * `IEditController2::openHelp(only_check)` to the Wine plugin host. + */ + struct OpenHelp { + using Response = UniversalTResult; + + native_size_t instance_id; + + TBool only_check; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value1b(only_check); + } + }; + + virtual tresult PLUGIN_API openHelp(TBool onlyCheck) override = 0; + + /** + * Message to pass through a call to + * `IEditController2::openAboutBox(only_check)` to the Wine plugin host. + */ + struct OpenAboutBox { + using Response = UniversalTResult; + + native_size_t instance_id; + + TBool only_check; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value1b(only_check); + } + }; + + virtual tresult PLUGIN_API openAboutBox(TBool onlyCheck) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/plugin/edit-controller.cpp b/src/common/serialization/vst3/plugin/edit-controller.cpp new file mode 100644 index 00000000..62fa2d03 --- /dev/null +++ b/src/common/serialization/vst3/plugin/edit-controller.cpp @@ -0,0 +1,27 @@ +// 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 . + +#include "edit-controller.h" + +YaEditController::ConstructArgs::ConstructArgs() {} + +YaEditController::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported( + Steinberg::FUnknownPtr(object)) {} + +YaEditController::YaEditController(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plugin/edit-controller.h b/src/common/serialization/vst3/plugin/edit-controller.h new file mode 100644 index 00000000..799573bd --- /dev/null +++ b/src/common/serialization/vst3/plugin/edit-controller.h @@ -0,0 +1,416 @@ +// 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 . + +#pragma once + +#include +#include + +#include "../../common.h" +#include "../base.h" +#include "../component-handler-proxy.h" +#include "../plug-view-proxy.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IEditController` for serialization purposes. This is + * instantiated as part of `Vst3PluginProxy`. + */ +class YaEditController : public Steinberg::Vst::IEditController { + public: + /** + * These are the arguments for creating a `YaEditController`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements + * `IEditController` and read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaEditController(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to + * `IEditController::setComponentState(state)` to the Wine plugin host. + */ + struct SetComponentState { + using Response = UniversalTResult; + + native_size_t instance_id; + + VectorStream state; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.object(state); + } + }; + + virtual tresult PLUGIN_API + setComponentState(Steinberg::IBStream* state) override = 0; + + // `setState()` and `getState()` are defiend in both `IComponent` and + // `IEditController`. Since an object can only ever implement one or the + // other, the messages for calling either are defined directly on + // `Vst3PluginProxy`. + virtual tresult PLUGIN_API + setState(Steinberg::IBStream* state) override = 0; + virtual tresult PLUGIN_API + getState(Steinberg::IBStream* state) override = 0; + + /** + * Message to pass through a call to `IEditController::getParameterCount()` + * to the Wine plugin host. + */ + struct GetParameterCount { + using Response = PrimitiveWrapper; + + native_size_t instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + } + }; + + virtual int32 PLUGIN_API getParameterCount() override = 0; + + /** + * The response code and returned parameter information for a call to + * `IEditController::getParameterInfo(param_index, &info)`. + */ + struct GetParameterInfoResponse { + UniversalTResult result; + Steinberg::Vst::ParameterInfo updated_info; + + template + void serialize(S& s) { + s.object(result); + s.object(updated_info); + } + }; + + /** + * Message to pass through a call to + * `IEditController::getParameterInfo(param_index, &info)` to the Wine + * plugin host. + */ + struct GetParameterInfo { + using Response = GetParameterInfoResponse; + + native_size_t instance_id; + + int32 param_index; + Steinberg::Vst::ParameterInfo info; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(param_index); + s.object(info); + } + }; + + virtual tresult PLUGIN_API + getParameterInfo(int32 paramIndex, + Steinberg::Vst::ParameterInfo& info /*out*/) override = 0; + + /** + * The response code and returned parameter information for a call to + * `IEditController::getParamStringByValue(id, value_normalized, + * &string)`. + */ + struct GetParamStringByValueResponse { + UniversalTResult result; + + std::u16string string; + + template + void serialize(S& s) { + s.object(result); + s.container2b(string, std::extent_v); + } + }; + + /** + * Message to pass through a call to + * `IEditController::getParamStringByValue(id, value_normalized, &string)` + * to the Wine plugin host. + */ + struct GetParamStringByValue { + using Response = GetParamStringByValueResponse; + + native_size_t instance_id; + + Steinberg::Vst::ParamID id; + Steinberg::Vst::ParamValue value_normalized; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(id); + s.value8b(value_normalized); + } + }; + + virtual tresult PLUGIN_API getParamStringByValue( + Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue valueNormalized /*in*/, + Steinberg::Vst::String128 string /*out*/) override = 0; + + /** + * The response code and returned parameter information for a call to + * `IEditController::getParamValueByString(id, string, + * &&value_normalized)`. + */ + struct GetParamValueByStringResponse { + UniversalTResult result; + + Steinberg::Vst::ParamValue value_normalized; + + template + void serialize(S& s) { + s.object(result); + s.value8b(value_normalized); + } + }; + + /** + * Message to pass through a call to + * `IEditController::getParamValueByString(id, string, &value_normalized)` + * to the Wine plugin host. + */ + struct GetParamValueByString { + using Response = GetParamValueByStringResponse; + + native_size_t instance_id; + + Steinberg::Vst::ParamID id; + std::u16string string; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(id); + s.container2b(string, std::extent_v); + } + }; + + virtual tresult PLUGIN_API getParamValueByString( + Steinberg::Vst::ParamID id, + Steinberg::Vst::TChar* string /*in*/, + Steinberg::Vst::ParamValue& valueNormalized /*out*/) override = 0; + + /** + * Message to pass through a call to + * `IEditController::normalizedParamToPlain(id, value_normalized)` to the + * Wine plugin host. + */ + struct NormalizedParamToPlain { + using Response = PrimitiveWrapper; + + native_size_t instance_id; + + Steinberg::Vst::ParamID id; + Steinberg::Vst::ParamValue value_normalized; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(id); + s.value8b(value_normalized); + } + }; + + virtual Steinberg::Vst::ParamValue PLUGIN_API normalizedParamToPlain( + Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue valueNormalized) override = 0; + + /** + * Message to pass through a call to + * `IEditController::plainParamToNormalized(id, plain_value)` to the Wine + * plugin host. + */ + struct PlainParamToNormalized { + using Response = PrimitiveWrapper; + + native_size_t instance_id; + + Steinberg::Vst::ParamID id; + Steinberg::Vst::ParamValue plain_value; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(id); + s.value8b(plain_value); + } + }; + + virtual Steinberg::Vst::ParamValue PLUGIN_API + plainParamToNormalized(Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue plainValue) override = 0; + + /** + * Message to pass through a call to + * `IEditController::getParamNormalized(id)` to the Wine plugin host. + */ + struct GetParamNormalized { + using Response = PrimitiveWrapper; + + native_size_t instance_id; + + Steinberg::Vst::ParamID id; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(id); + } + }; + + virtual Steinberg::Vst::ParamValue PLUGIN_API + getParamNormalized(Steinberg::Vst::ParamID id) override = 0; + + /** + * Message to pass through a call to + * `IEditController::setParamNormalized(id, value)` to the Wine plugin host. + */ + struct SetParamNormalized { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::ParamID id; + Steinberg::Vst::ParamValue value; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(id); + s.value8b(value); + } + }; + + virtual tresult PLUGIN_API + setParamNormalized(Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue value) override = 0; + + /** + * Message to pass through a call to + * `IEditController::setComponentHandler(handler)` to the Wine plugin host. + * Like when creating a proxy for a plugin object, we'll read all supported + * interfaces form the component handler instance passed by the host. We'll + * then create a perfect proxy on the plugin side, that can do callbacks to + * the actual component handler passed by the host. + */ + struct SetComponentHandler { + using Response = UniversalTResult; + + native_size_t instance_id; + + Vst3ComponentHandlerProxy::ConstructArgs component_handler_proxy_args; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.object(component_handler_proxy_args); + } + }; + + virtual tresult PLUGIN_API setComponentHandler( + Steinberg::Vst::IComponentHandler* handler) override = 0; + + /** + * The `IPlugView` proxy arguments returned from a call to + * `IEditController::createView(name)`. If not empty, then we'll use this to + * construct a proxy object that can send control messages to the plugin + * instance's actual `IPlugView` object. + */ + struct CreateViewResponse { + std::optional plug_view_args; + + template + void serialize(S& s) { + s.ext(plug_view_args, bitsery::ext::StdOptional{}); + } + }; + + /** + * Message to pass through a call to `IEditController::createView(name)` to + * the Wine plugin host. + */ + struct CreateView { + using Response = CreateViewResponse; + + native_size_t instance_id; + + std::string name; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.text1b(name, 128); + } + }; + + virtual Steinberg::IPlugView* PLUGIN_API + createView(Steinberg::FIDString name) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop + +namespace Steinberg { +namespace Vst { +template +void serialize(S& s, ParameterInfo& info) { + s.value4b(info.id); + s.container2b(info.title); + s.container2b(info.shortTitle); + s.container2b(info.units); + s.value4b(info.stepCount); + s.value8b(info.defaultNormalizedValue); + s.value4b(info.unitId); + s.value4b(info.flags); +} +} // namespace Vst +} // namespace Steinberg diff --git a/src/common/serialization/vst3/plugin/plugin-base.cpp b/src/common/serialization/vst3/plugin/plugin-base.cpp new file mode 100644 index 00000000..e2abb0fd --- /dev/null +++ b/src/common/serialization/vst3/plugin/plugin-base.cpp @@ -0,0 +1,26 @@ +// 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 . + +#include "plugin-base.h" + +YaPluginBase::ConstructArgs::ConstructArgs() {} + +YaPluginBase::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported(Steinberg::FUnknownPtr(object)) {} + +YaPluginBase::YaPluginBase(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plugin/plugin-base.h b/src/common/serialization/vst3/plugin/plugin-base.h new file mode 100644 index 00000000..5958ac9b --- /dev/null +++ b/src/common/serialization/vst3/plugin/plugin-base.h @@ -0,0 +1,111 @@ +// 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 . + +#pragma once + +#include + +#include "../../common.h" +#include "../base.h" +#include "../host-context-proxy.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IPluginBase` for serialization purposes. Both components and + * edit controllers inherit from this. This is instantiated as part of + * `Vst3PluginProxy`. + */ +class YaPluginBase : public Steinberg::IPluginBase { + public: + /** + * These are the arguments for creating a `YaPluginBase`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements `IPluginBase` and + * read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaPluginBase(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to `IPluginBase::initialize()` to the Wine + * plugin host. We will read what interfaces the passed context object + * implements so we can then create a proxy object on the Wine side that the + * plugin can use to make callbacks with. The lifetime of this + * `Vst3HostContextProxy` object should be bound to the `IComponent` we are + * proxying. + */ + struct Initialize { + using Response = UniversalTResult; + + native_size_t instance_id; + + Vst3HostContextProxy::ConstructArgs host_context_args; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.object(host_context_args); + } + }; + + virtual tresult PLUGIN_API initialize(FUnknown* context) override = 0; + + /** + * Message to pass through a call to `IPluginBase::terminate()` to the Wine + * plugin host. + */ + struct Terminate { + using Response = UniversalTResult; + + native_size_t instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + } + }; + + virtual tresult PLUGIN_API terminate() override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/plugin/program-list-data.cpp b/src/common/serialization/vst3/plugin/program-list-data.cpp new file mode 100644 index 00000000..3e76e812 --- /dev/null +++ b/src/common/serialization/vst3/plugin/program-list-data.cpp @@ -0,0 +1,27 @@ +// 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 . + +#include "program-list-data.h" + +YaProgramListData::ConstructArgs::ConstructArgs() {} + +YaProgramListData::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported( + Steinberg::FUnknownPtr(object)) {} + +YaProgramListData::YaProgramListData(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plugin/program-list-data.h b/src/common/serialization/vst3/plugin/program-list-data.h new file mode 100644 index 00000000..434441be --- /dev/null +++ b/src/common/serialization/vst3/plugin/program-list-data.h @@ -0,0 +1,159 @@ +// 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 . + +#pragma once + +#include + +#include "../../common.h" +#include "../base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IProgramListData` for serialization purposes. This is + * instantiated as part of `Vst3PluginProxy`. + */ +class YaProgramListData : public Steinberg::Vst::IProgramListData { + public: + /** + * These are the arguments for creating a `YaProgramListData`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements + * `IProgramListData` and read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaProgramListData(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to + * `IProgramListData::programDataSupported(list_id)` to the Wine plugin + * host. + */ + struct ProgramDataSupported { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::ProgramListID list_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(list_id); + } + }; + + virtual tresult PLUGIN_API + programDataSupported(Steinberg::Vst::ProgramListID listId) override = 0; + + /** + * The response code and written state for a call to + * `IProgramListData::getProgramData(list_id, program_index, &data)`. + */ + struct GetProgramDataResponse { + UniversalTResult result; + VectorStream data; + + template + void serialize(S& s) { + s.object(result); + s.object(data); + } + }; + + /** + * Message to pass through a call to + * `IProgramListData::getProgramData(list_id, program_index, &data)` to the + * Wine plugin host. + */ + struct GetProgramData { + using Response = GetProgramDataResponse; + + native_size_t instance_id; + + Steinberg::Vst::ProgramListID list_id; + int32 program_index; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(list_id); + s.value4b(program_index); + } + }; + + virtual tresult PLUGIN_API + getProgramData(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::IBStream* data) override = 0; + + /** + * Message to pass through a call to + * `IProgramListData::SetProgramData(list_id, program_index, data)` to the + * Wine plugin host. + */ + struct SetProgramData { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::ProgramListID list_id; + int32 program_index; + VectorStream data; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(list_id); + s.value4b(program_index); + s.object(data); + } + }; + + virtual tresult PLUGIN_API + setProgramData(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::IBStream* data) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/plugin/unit-data.cpp b/src/common/serialization/vst3/plugin/unit-data.cpp new file mode 100644 index 00000000..b03cd05a --- /dev/null +++ b/src/common/serialization/vst3/plugin/unit-data.cpp @@ -0,0 +1,27 @@ +// 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 . + +#include "unit-data.h" + +YaUnitData::ConstructArgs::ConstructArgs() {} + +YaUnitData::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported( + Steinberg::FUnknownPtr(object)) {} + +YaUnitData::YaUnitData(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plugin/unit-data.h b/src/common/serialization/vst3/plugin/unit-data.h new file mode 100644 index 00000000..bd200e2b --- /dev/null +++ b/src/common/serialization/vst3/plugin/unit-data.h @@ -0,0 +1,150 @@ +// 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 . + +#pragma once + +#include + +#include "../../common.h" +#include "../base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IUnitData` for serialization purposes. This is instantiated as + * part of `Vst3PluginProxy`. + */ +class YaUnitData : public Steinberg::Vst::IUnitData { + public: + /** + * These are the arguments for creating a `YaUnitData`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements `IUnitData` and + * read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaUnitData(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to `IUnitData::unitDataSupported(unit_id)` + * to the Wine plugin host. + */ + struct UnitDataSupported { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::UnitID unit_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(unit_id); + } + }; + + virtual tresult PLUGIN_API + unitDataSupported(Steinberg::Vst::UnitID unitId) override = 0; + + /** + * The response code and written state for a call to + * `IUnitData::getUnitData(unit_id, &data)`. + */ + struct GetUnitDataResponse { + UniversalTResult result; + VectorStream data; + + template + void serialize(S& s) { + s.object(result); + s.object(data); + } + }; + + /** + * Message to pass through a call to `IUnitData::getUnitData(unit_id, + * &data)` to the Wine plugin host. + */ + struct GetUnitData { + using Response = GetUnitDataResponse; + + native_size_t instance_id; + + Steinberg::Vst::UnitID unit_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(unit_id); + } + }; + + virtual tresult PLUGIN_API + getUnitData(Steinberg::Vst::UnitID unitId, + Steinberg::IBStream* data) override = 0; + + /** + * Message to pass through a call to `IUnitData::SetUnitData(unit_id, data)` + * to the Wine plugin host. + */ + struct SetUnitData { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::UnitID unit_id; + VectorStream data; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(unit_id); + s.object(data); + } + }; + + virtual tresult PLUGIN_API + setUnitData(Steinberg::Vst::UnitID unitId, + Steinberg::IBStream* data) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop diff --git a/src/common/serialization/vst3/plugin/unit-info.cpp b/src/common/serialization/vst3/plugin/unit-info.cpp new file mode 100644 index 00000000..8f8cf098 --- /dev/null +++ b/src/common/serialization/vst3/plugin/unit-info.cpp @@ -0,0 +1,26 @@ +// 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 . + +#include "unit-info.h" + +YaUnitInfo::ConstructArgs::ConstructArgs() {} + +YaUnitInfo::ConstructArgs::ConstructArgs( + Steinberg::IPtr object) + : supported(Steinberg::FUnknownPtr(object)) {} + +YaUnitInfo::YaUnitInfo(const ConstructArgs&& args) + : arguments(std::move(args)) {} diff --git a/src/common/serialization/vst3/plugin/unit-info.h b/src/common/serialization/vst3/plugin/unit-info.h new file mode 100644 index 00000000..1708336a --- /dev/null +++ b/src/common/serialization/vst3/plugin/unit-info.h @@ -0,0 +1,462 @@ +// 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 . + +#pragma once + +#include + +#include "../../common.h" +#include "../base.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" + +/** + * Wraps around `IUnitInfo` for serialization purposes. This is instantiated as + * part of `Vst3PluginProxy`. + */ +class YaUnitInfo : public Steinberg::Vst::IUnitInfo { + public: + /** + * These are the arguments for creating a `YaUnitInfo`. + */ + struct ConstructArgs { + ConstructArgs(); + + /** + * Check whether an existing implementation implements `IUnitInfo` and + * read arguments from it. + */ + ConstructArgs(Steinberg::IPtr object); + + /** + * Whether the object supported this interface. + */ + bool supported; + + template + void serialize(S& s) { + s.value1b(supported); + } + }; + + /** + * Instantiate this instance with arguments read from another interface + * implementation. + */ + YaUnitInfo(const ConstructArgs&& args); + + inline bool supported() const { return arguments.supported; } + + /** + * Message to pass through a call to `IUnitInfo::getUnitCount()` to the Wine + * plugin host. + */ + struct GetUnitCount { + using Response = PrimitiveWrapper; + + native_size_t instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + } + }; + + virtual int32 PLUGIN_API getUnitCount() override = 0; + + /** + * The response code and returned unit information for a call to + * `IUnitInfo::getUnitInfo(unit_index, &info)`. + */ + struct GetUnitInfoResponse { + UniversalTResult result; + Steinberg::Vst::UnitInfo info; + + template + void serialize(S& s) { + s.object(result); + s.object(info); + } + }; + + /** + * Message to pass through a call to `IUnitInfo::getUnitInfo(unit_index, + * &info)` to the Wine plugin host. + */ + struct GetUnitInfo { + using Response = GetUnitInfoResponse; + + native_size_t instance_id; + + int32 unit_index; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(unit_index); + } + }; + + virtual tresult PLUGIN_API + getUnitInfo(int32 unitIndex, + Steinberg::Vst::UnitInfo& info /*out*/) override = 0; + + /** + * Message to pass through a call to `IUnitInfo::getProgramListCount()` to + * the Wine plugin host. + */ + struct GetProgramListCount { + using Response = PrimitiveWrapper; + + native_size_t instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + } + }; + + virtual int32 PLUGIN_API getProgramListCount() override = 0; + + /** + * The response code and returned unit information for a call to + * `IUnitInfo::getProgramListInfo(list_index, &info)`. + */ + struct GetProgramListInfoResponse { + UniversalTResult result; + Steinberg::Vst::ProgramListInfo info; + + template + void serialize(S& s) { + s.object(result); + s.object(info); + } + }; + + /** + * Message to pass through a call to + * `IUnitInfo::getProgramListInfo(list_index, &info)` to the Wine plugin + * host. + */ + struct GetProgramListInfo { + using Response = GetProgramListInfoResponse; + + native_size_t instance_id; + + int32 list_index; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(list_index); + } + }; + + virtual tresult PLUGIN_API getProgramListInfo( + int32 listIndex, + Steinberg::Vst::ProgramListInfo& info /*out*/) override = 0; + + /** + * The response code and returned name for a call to + * `IUnitInfo::getProgramName(list_id, program_index, &name)`. + */ + struct GetProgramNameResponse { + UniversalTResult result; + std::u16string name; + + template + void serialize(S& s) { + s.object(result); + s.text2b(name, std::extent_v); + } + }; + + /** + * Message to pass through a call to `IUnitInfo::getProgramName(list_id, + * program_index, &name)` to the Wine plugin host. + */ + struct GetProgramName { + using Response = GetProgramNameResponse; + + native_size_t instance_id; + + Steinberg::Vst::ProgramListID list_id; + int32 program_index; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(list_id); + s.value4b(program_index); + } + }; + + virtual tresult PLUGIN_API + getProgramName(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::Vst::String128 name /*out*/) override = 0; + + /** + * The response code and returned value for a call to + * `IUnitInfo::getPrograminfo(list_id, program_index, attribute_name, + * &attribute_value)`. + */ + struct GetProgramInfoResponse { + UniversalTResult result; + std::u16string attribute_value; + + template + void serialize(S& s) { + s.object(result); + s.text2b(attribute_value, std::extent_v); + } + }; + + /** + * Message to pass through a call to `IUnitInfo::getProgramInfo(list_id, + * program_index, attribute_id, &attribute_value)` to the Wine plugin host. + */ + struct GetProgramInfo { + using Response = GetProgramInfoResponse; + + native_size_t instance_id; + + Steinberg::Vst::ProgramListID list_id; + int32 program_index; + std::string attribute_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(list_id); + s.value4b(program_index); + s.text1b(attribute_id, 256); + } + }; + + virtual tresult PLUGIN_API getProgramInfo( + Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::Vst::CString attributeId /*in*/, + Steinberg::Vst::String128 attributeValue /*out*/) override = 0; + + /** + * Message to pass through a call to + * `IUnitInfo::hasProgramPitchNames(list_id, program_index)` to the Wine + * plugin host. + */ + struct HasProgramPitchNames { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::ProgramListID list_id; + int32 program_index; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(list_id); + s.value4b(program_index); + } + }; + + virtual tresult PLUGIN_API + hasProgramPitchNames(Steinberg::Vst::ProgramListID listId, + int32 programIndex) override = 0; + + /** + * The response code and returned name for a call to + * `IUnitInfo::getProgramPitchName(list_id, program_index, midi_pitch, + * &name)`. + */ + struct GetProgramPitchNameResponse { + UniversalTResult result; + std::u16string name; + + template + void serialize(S& s) { + s.object(result); + s.text2b(name, std::extent_v); + } + }; + + /** + * Message to pass through a call to + * `IUnitInfo::getProgramPitchName(list_id, program_index, midi_pitch, + * &name)` to the Wine plugin host. + */ + struct GetProgramPitchName { + using Response = GetProgramPitchNameResponse; + + native_size_t instance_id; + + Steinberg::Vst::ProgramListID list_id; + int32 program_index; + int16 midi_pitch; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(list_id); + s.value4b(program_index); + s.value2b(midi_pitch); + } + }; + + virtual tresult PLUGIN_API + getProgramPitchName(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + int16 midiPitch, + Steinberg::Vst::String128 name /*out*/) override = 0; + + /** + * Message to pass through a call to `IUnitInfo::getSelectedUnit()` to the + * Wine plugin host. + */ + struct GetSelectedUnit { + using Response = PrimitiveWrapper; + + native_size_t instance_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + } + }; + + virtual Steinberg::Vst::UnitID PLUGIN_API getSelectedUnit() override = 0; + + /** + * Message to pass through a call to `IUnitInfo::selectUnit(unit_id)` to the + * Wine plugin host. + */ + struct SelectUnit { + using Response = UniversalTResult; + + native_size_t instance_id; + + Steinberg::Vst::UnitID unit_id; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(unit_id); + } + }; + + virtual tresult PLUGIN_API + selectUnit(Steinberg::Vst::UnitID unitId) override = 0; + + /** + * The response code and returned unit ID for a call to + * `IUnitInfo::getUnitByBus(type, dir, bus_index, channel, &unit_id)`. + */ + struct GetUnitByBusResponse { + UniversalTResult result; + Steinberg::Vst::UnitID unit_id; + + template + void serialize(S& s) { + s.object(result); + s.value4b(unit_id); + } + }; + + /** + * Message to pass through a call to `IUnitInfo::getUnitByBus(type, dir, + * bus_index, channel, &unit_id)` to the Wine plugin host. + */ + struct GetUnitByBus { + using Response = GetUnitByBusResponse; + + native_size_t instance_id; + + Steinberg::Vst::MediaType type; + Steinberg::Vst::BusDirection dir; + int32 bus_index; + int32 channel; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(type); + s.value4b(dir); + s.value4b(bus_index); + s.value4b(channel); + } + }; + + virtual tresult PLUGIN_API + getUnitByBus(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir, + int32 busIndex, + int32 channel, + Steinberg::Vst::UnitID& unitId /*out*/) override = 0; + + /* + * Message to pass through a call to + * `IUnitInfo::setUnitProgramData(list_or_unit_id, program_index, data)` to + * the Wine plugin host. + */ + struct SetUnitProgramData { + using Response = UniversalTResult; + + native_size_t instance_id; + + int32 list_or_unit_id; + int32 program_index; + VectorStream data; + + template + void serialize(S& s) { + s.value8b(instance_id); + s.value4b(list_or_unit_id); + s.value4b(program_index); + s.object(data); + } + }; + + virtual tresult PLUGIN_API + setUnitProgramData(int32 listOrUnitId, + int32 programIndex, + Steinberg::IBStream* data) override = 0; + + protected: + ConstructArgs arguments; +}; + +#pragma GCC diagnostic pop + +namespace Steinberg { +namespace Vst { +template +void serialize(S& s, UnitInfo& info) { + s.value4b(info.id); + s.value4b(info.parentUnitId); + s.text2b(info.name); + s.value4b(info.programListId); +} + +template +void serialize(S& s, ProgramListInfo& info) { + s.value4b(info.id); + s.text2b(info.name); + s.value4b(info.programCount); +} +} // namespace Vst +} // namespace Steinberg diff --git a/src/common/serialization/vst3/process-data.cpp b/src/common/serialization/vst3/process-data.cpp new file mode 100644 index 00000000..769470ff --- /dev/null +++ b/src/common/serialization/vst3/process-data.cpp @@ -0,0 +1,240 @@ +// 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 . + +#include "process-data.h" + +#include "src/common/utils.h" + +YaAudioBusBuffers::YaAudioBusBuffers() {} + +YaAudioBusBuffers::YaAudioBusBuffers(int32 sample_size, + size_t num_channels, + size_t num_samples) + : buffers(sample_size == Steinberg::Vst::SymbolicSampleSizes::kSample64 + ? decltype(buffers)(std::vector>( + num_channels, + std::vector(num_samples, 0.0))) + : decltype(buffers)(std::vector>( + num_channels, + std::vector(num_samples, 0.0)))) {} + +YaAudioBusBuffers::YaAudioBusBuffers( + int32 sample_size, + int32 num_samples, + const Steinberg::Vst::AudioBusBuffers& data) + : silence_flags(data.silenceFlags) { + switch (sample_size) { + case Steinberg::Vst::kSample64: { + std::vector> vector_buffers(data.numChannels); + for (int channel = 0; channel < data.numChannels; channel++) { + vector_buffers[channel].assign( + &data.channelBuffers64[channel][0], + &data.channelBuffers64[channel][num_samples]); + } + + buffers = std::move(vector_buffers); + } break; + case Steinberg::Vst::kSample32: + // I don't think they'll add any other sample sizes any time soon + default: { + std::vector> vector_buffers(data.numChannels); + for (int channel = 0; channel < data.numChannels; channel++) { + vector_buffers[channel].assign( + &data.channelBuffers32[channel][0], + &data.channelBuffers32[channel][num_samples]); + } + + buffers = std::move(vector_buffers); + } break; + } +} + +Steinberg::Vst::AudioBusBuffers YaAudioBusBuffers::get() { + Steinberg::Vst::AudioBusBuffers reconstructed_buffers; + reconstructed_buffers.silenceFlags = silence_flags; + std::visit(overload{ + [&](std::vector>& buffers) { + buffer_pointers.clear(); + for (auto& buffer : buffers) { + buffer_pointers.push_back(buffer.data()); + } + + reconstructed_buffers.numChannels = buffers.size(); + reconstructed_buffers.channelBuffers64 = + reinterpret_cast(buffer_pointers.data()); + }, + [&](std::vector>& buffers) { + buffer_pointers.clear(); + for (auto& buffer : buffers) { + buffer_pointers.push_back(buffer.data()); + } + + reconstructed_buffers.numChannels = buffers.size(); + reconstructed_buffers.channelBuffers32 = + reinterpret_cast(buffer_pointers.data()); + }, + }, + buffers); + + return reconstructed_buffers; +} + +size_t YaAudioBusBuffers::num_channels() const { + return std::visit([&](const auto& buffers) { return buffers.size(); }, + buffers); +} + +void YaAudioBusBuffers::write_back_outputs( + Steinberg::Vst::AudioBusBuffers& output_buffers) const { + output_buffers.silenceFlags = silence_flags; + std::visit( + overload{ + [&](const std::vector>& buffers) { + for (int channel = 0; channel < output_buffers.numChannels; + channel++) { + std::copy(buffers[channel].begin(), buffers[channel].end(), + output_buffers.channelBuffers64[channel]); + } + }, + [&](const std::vector>& buffers) { + for (int channel = 0; channel < output_buffers.numChannels; + channel++) { + std::copy(buffers[channel].begin(), buffers[channel].end(), + output_buffers.channelBuffers32[channel]); + } + }, + }, + buffers); +} + +void YaProcessDataResponse::write_back_outputs( + Steinberg::Vst::ProcessData& process_data) { + for (int i = 0; i < process_data.numOutputs; i++) { + outputs[i].write_back_outputs(process_data.outputs[i]); + } + + if (output_parameter_changes && process_data.outputParameterChanges) { + output_parameter_changes->write_back_outputs( + *process_data.outputParameterChanges); + } + + if (output_events && process_data.outputEvents) { + output_events->write_back_outputs(*process_data.outputEvents); + } +} + +YaProcessData::YaProcessData() {} + +YaProcessData::YaProcessData(const Steinberg::Vst::ProcessData& process_data) + : process_mode(process_data.processMode), + symbolic_sample_size(process_data.symbolicSampleSize), + num_samples(process_data.numSamples), + outputs_num_channels(process_data.numOutputs), + // Even though `ProcessData::inputParamterChanges` is mandatory, the VST3 + // validator will pass a null pointer here + input_parameter_changes( + process_data.inputParameterChanges + ? YaParameterChanges(*process_data.inputParameterChanges) + : YaParameterChanges()), + output_parameter_changes_supported(process_data.outputParameterChanges), + input_events(process_data.inputEvents ? std::make_optional( + *process_data.inputEvents) + : std::nullopt), + output_events_supported(process_data.outputEvents), + process_context(process_data.processContext + ? std::make_optional( + *process_data.processContext) + : std::nullopt) { + for (int i = 0; i < process_data.numInputs; i++) { + inputs.emplace_back(symbolic_sample_size, num_samples, + process_data.inputs[i]); + } + + // Fetch the number of channels for each output so we can recreate these + // buffers in the Wine plugin host + for (int i = 0; i < process_data.numOutputs; i++) { + outputs_num_channels[i] = process_data.outputs[i].numChannels; + } +} + +Steinberg::Vst::ProcessData& YaProcessData::get() { + // We'll have to transform out `YaAudioBusBuffers` objects into an array of + // `AudioBusBuffers` object so the plugin can deal with them. These objects + // contain pointers to those original objects and thus don't store any + // buffer data themselves. + inputs_audio_bus_buffers.clear(); + for (auto& buffers : inputs) { + inputs_audio_bus_buffers.push_back(buffers.get()); + } + + // We'll do the same with with the outputs, but we'll first have to + // initialize zeroed out buffers for the plugin to work with since we didn't + // serialize those directly + outputs.clear(); + outputs_audio_bus_buffers.clear(); + for (auto& num_channels : outputs_num_channels) { + YaAudioBusBuffers& buffers = outputs.emplace_back( + symbolic_sample_size, num_channels, num_samples); + outputs_audio_bus_buffers.push_back(buffers.get()); + } + + reconstructed_process_data.processMode = process_mode; + reconstructed_process_data.symbolicSampleSize = symbolic_sample_size; + reconstructed_process_data.numSamples = num_samples; + reconstructed_process_data.numInputs = inputs.size(); + reconstructed_process_data.numOutputs = outputs_num_channels.size(); + reconstructed_process_data.inputs = inputs_audio_bus_buffers.data(); + reconstructed_process_data.outputs = outputs_audio_bus_buffers.data(); + + reconstructed_process_data.inputParameterChanges = &input_parameter_changes; + if (output_parameter_changes_supported) { + output_parameter_changes.emplace(); + reconstructed_process_data.outputParameterChanges = + &*output_parameter_changes; + } else { + output_parameter_changes.reset(); + reconstructed_process_data.outputParameterChanges = nullptr; + } + + if (input_events) { + reconstructed_process_data.inputEvents = &*input_events; + } else { + reconstructed_process_data.inputEvents = nullptr; + } + + if (output_events_supported) { + output_events.emplace(); + reconstructed_process_data.outputEvents = &*output_events; + } else { + output_events.reset(); + reconstructed_process_data.outputEvents = nullptr; + } + + if (process_context) { + reconstructed_process_data.processContext = &*process_context; + } else { + reconstructed_process_data.processContext = nullptr; + } + + return reconstructed_process_data; +} + +YaProcessDataResponse YaProcessData::move_outputs_to_response() { + return YaProcessDataResponse{ + .outputs = std::move(outputs), + .output_parameter_changes = std::move(output_parameter_changes), + .output_events = std::move(output_events)}; +} diff --git a/src/common/serialization/vst3/process-data.h b/src/common/serialization/vst3/process-data.h new file mode 100644 index 00000000..846244ea --- /dev/null +++ b/src/common/serialization/vst3/process-data.h @@ -0,0 +1,369 @@ +// 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 . + +#pragma once + +#include + +#include +#include +#include + +#include "base.h" +#include "event-list.h" +#include "parameter-changes.h" + +// This header provides serialization wrappers around `ProcessData` + +/** + * A serializable wrapper around `AudioBusBuffers` back by `std::vector`s. + * Data can be read from a `AudioBusBuffers` object provided by the host, and + * one the Wine plugin host side we can reconstruct the `AudioBusBuffers` object + * back from this object again. + * + * @see YaProcessData + */ +class YaAudioBusBuffers { + public: + /** + * A default constructor does not make any sense here since the actual data + * is a union, but we need a default constructor for bitsery. + */ + YaAudioBusBuffers(); + + /** + * Create a new, zero initialize audio bus buffers object. Used to + * reconstruct the output buffers during `YaProcessData::get()`. + */ + YaAudioBusBuffers(int32 sample_size, + size_t num_channels, + size_t num_samples); + + /** + * Copy data from a host provided `AudioBusBuffers` object during a process + * call. Constructed as part of `YaProcessData`. Since `AudioBusBuffers` + * contains an untagged union for storing single and double precision + * floating point values, the original `ProcessData`'s `symbolicSampleSize` + * field determines which variant of that union to use. Similarly the + * `ProcessData`' `numSamples` field determines the extent of these arrays. + */ + YaAudioBusBuffers(int32 sample_size, + int32 num_samples, + const Steinberg::Vst::AudioBusBuffers& data); + + /** + * Reconstruct the original `AudioBusBuffers` object passed to the + * constructor and return it. This is used as part of + * `YaProcessData::get()`. The object contains pointers to `buffers`, so it + * may not outlive this object. + */ + Steinberg::Vst::AudioBusBuffers get(); + + /** + * Return the number of channels in `buffers`. Only used for debug logs. + */ + size_t num_channels() const; + + /** + * Write these buffers and the silence flag back to an `AudioBusBuffers + * object provided by the host. + */ + void write_back_outputs( + Steinberg::Vst::AudioBusBuffers& output_buffers) const; + + template + void serialize(S& s) { + s.value8b(silence_flags); + s.ext(buffers, bitsery::ext::StdVariant{ + [](S& s, std::vector>& buffers) { + s.container(buffers, max_num_speakers, + [](S& s, auto& channel) { + s.container4b(channel, 1 << 16); + }); + }, + [](S& s, std::vector>& buffers) { + s.container(buffers, max_num_speakers, + [](S& s, auto& channel) { + s.container8b(channel, 1 << 16); + }); + }, + }); + } + + private: + /** + * We need these during the reconstruction process to provide a pointer to + * an array of pointers to the actual buffers. + */ + std::vector buffer_pointers; + + /** + * A bitfield for silent channels copied directly from the input struct. + * + * We could have done some optimizations to avoid unnecessary copying when + * these silence flags are set, but since it's an optional feature we + * shouldn't risk it. + */ + uint64 silence_flags; + + /** + * The original implementation uses heap arrays and it stores a + * {float,double} array pointer per channel, with a separate field for the + * number of channels. We'll store this using a vector of vectors. + */ + std::variant>, + std::vector>> + buffers; +}; + +/** + * A serializable wrapper around the output fields of `ProcessData`. We send + * this back as a response to a process call so we can write those fields back + * to the host. It would be possible to just send `YaProcessData` back and have + * everything be in a single structure, but that would involve a lot of + * unnecessary copying (since, at least in theory, all the input audio buffers, + * events and context data shouldn't have been changed by the plugin). + * + * @see YaProcessData + */ +struct YaProcessDataResponse { + // These fields are directly moved from a `YaProcessData` object. See the + // docstrings there for more information + std::vector outputs; + std::optional output_parameter_changes; + std::optional output_events; + + /** + * Write all of this output data back to the host's `ProcessData` object. + */ + void write_back_outputs(Steinberg::Vst::ProcessData& process_data); + + template + void serialize(S& s) { + s.container(outputs, max_num_speakers); + s.ext(output_parameter_changes, bitsery::ext::StdOptional{}); + s.ext(output_events, bitsery::ext::StdOptional{}); + } +}; + +/** + * A serializable wrapper around `ProcessData`. We'll read all information from + * the host so we can serialize it and provide an equivalent `ProcessData` + * struct to the plugin. Then we can create a `YaProcessDataResponse` object + * that contains all output values so we can write those back to the host. + */ +class YaProcessData { + public: + YaProcessData(); + + /** + * Copy data from a host provided `ProcessData` object during a process + * call. This struct can then be serialized, and `YaProcessData::get()` can + * then be used again to recreate the original `ProcessData` object. + */ + YaProcessData(const Steinberg::Vst::ProcessData& process_data); + + /** + * Reconstruct the original `ProcessData` object passed to the constructor + * and return it. This is used in the Wine plugin host when processing an + * `IAudioProcessor::process()` call. + */ + Steinberg::Vst::ProcessData& get(); + + /** + * **Move** all output written by the Windows VST3 plugin to a response + * object that can be used to write those results back to the host. This + * should of course be done after making a call to the `IAudioProcessor`'s + * `process()` function with the object obtained using `get()`. + */ + YaProcessDataResponse move_outputs_to_response(); + + template + void serialize(S& s) { + s.value4b(process_mode); + s.value4b(symbolic_sample_size); + s.value4b(num_samples); + s.container(inputs, max_num_speakers); + s.container4b(outputs_num_channels, max_num_speakers); + s.object(input_parameter_changes); + s.value1b(output_parameter_changes_supported); + s.ext(input_events, bitsery::ext::StdOptional{}); + s.value1b(output_events_supported); + s.ext(process_context, bitsery::ext::StdOptional{}); + + // We of course won't serialize the `reconstructed_process_data` and all + // of the `output*` fields defined below it + } + + // These fields are input and context data read from the original + // `ProcessData` object + + /** + * The processing mode copied directly from the input struct. + */ + int32 process_mode; + + /** + * The symbolic sample size (see `Steinberg::Vst::SymbolicSampleSizes`) is + * important. The audio buffers are represented by as a C-style untagged + * union of array of either single or double precision floating point + * arrays. This field determines which of those variants should be used. + */ + int32 symbolic_sample_size; + + /** + * The number of samples in each audio buffer. + */ + int32 num_samples; + + /** + * In `ProcessData` they use C-style heap arrays, so they have to store the + * number of input/output busses, and then also store pointers to the first + * audio buffer object. We can combine these two into vectors. + */ + std::vector inputs; + + /** + * For the outputs we only have to keep track of how many output channels + * each bus has. From this and from `num_samples` we can reconstruct the + * output buffers on the Wine side of the process call. + */ + std::vector outputs_num_channels; + + /** + * Incoming parameter changes. + */ + YaParameterChanges input_parameter_changes; + + /** + * Whether the host supports output parameter changes (depending on whether + * `outputParameterChanges` was a null pointer or not). + */ + bool output_parameter_changes_supported; + + /** + * Incoming events. + */ + std::optional input_events; + + /** + * Whether the host supports output events (depending on whether + * `outputEvents` was a null pointer or not). + */ + bool output_events_supported; + + /** + * Some more information about the project and transport. + */ + std::optional process_context; + + private: + // These are the same fields as in `YaProcessDataResponse`. We'll generate + // these as part of creating `reconstructed_process_data`, and they will be + // moved into a response object during `move_outputs_to_response()`. + + /** + * The outputs. Will be created based on `outputs_num_channels` (which + * determines how many output busses there are and how many channels each + * bus has) and `num_samples`. + */ + std::vector outputs; + + /** + * The output parameter changes. Will be initialized depending on + * `output_parameter_changes_supported`. + */ + std::optional output_parameter_changes; + + /** + * The output events. Will be initialized depending on + * `output_events_supported`. + */ + std::optional output_events; + + // These last few members are used on the Wine plugin host side to + // reconstruct the original `ProcessData` object. Here we also initialize + // these `output*` fields so the Windows VST3 plugin can write to them + // though a regular `ProcessData` object. Finally we can wrap these output + // fields back into a `YaProcessDataResponse` using + // `move_outputs_to_response()`. so they can be serialized and written back + // to the host's `ProcessData` object. + + /** + * Obtained by calling `.get()` on every `YaAudioBusBuffers` object in + * `intputs`. These objects contain pointers to the data in `inputs` and may + * thus not outlive them. + */ + std::vector inputs_audio_bus_buffers; + + /** + * Obtained by calling `.get()` on every `YaAudioBusBuffers` object in + * `outputs`. These objects contain pointers to the data in `outputs` and + * may thus not outlive them. These are created in a two step process, since + * we first have to create `outputs` from `outputs_num_channels` before we + * can transform it into a structure the Windows VST3 plugin can work with. + * Hooray for heap arrays. + */ + std::vector outputs_audio_bus_buffers; + + /** + * The process data we reconstruct from the other fields during `get()`. + */ + Steinberg::Vst::ProcessData reconstructed_process_data; +}; + +namespace Steinberg { +namespace Vst { +template +void serialize(S& s, Steinberg::Vst::ProcessContext& process_context) { + // The docs don't mention that things ever got added to this context (and + // that some fields thus may not exist for all hosts), so we'll just + // directly serialize everything. If it does end up being the case that new + // fields were added here we should serialize based on the bits set in the + // flags bitfield. + s.value4b(process_context.state); + s.value8b(process_context.sampleRate); + s.value8b(process_context.projectTimeSamples); + s.value8b(process_context.systemTime); + s.value8b(process_context.continousTimeSamples); + s.value8b(process_context.projectTimeMusic); + s.value8b(process_context.barPositionMusic); + s.value8b(process_context.cycleStartMusic); + s.value8b(process_context.cycleEndMusic); + s.value8b(process_context.tempo); + s.value4b(process_context.timeSigNumerator); + s.value4b(process_context.timeSigDenominator); + s.object(process_context.chord); + s.value4b(process_context.smpteOffsetSubframes); + s.value4b(process_context.smpteOffsetSubframes); + s.object(process_context.frameRate); + s.value4b(process_context.samplesToNextClock); +} + +template +void serialize(S& s, Steinberg::Vst::Chord& chord) { + s.value1b(chord.keyNote); + s.value1b(chord.rootNote); + s.value2b(chord.chordMask); +} + +template +void serialize(S& s, Steinberg::Vst::FrameRate& frame_rate) { + s.value4b(frame_rate.framesPerSecond); + s.value4b(frame_rate.flags); +} +} // namespace Vst +} // namespace Steinberg diff --git a/src/common/utils.h b/src/common/utils.h index a21fb0dd..909411b6 100644 --- a/src/common/utils.h +++ b/src/common/utils.h @@ -21,6 +21,15 @@ #endif #include +// The cannonical overloading template for `std::visitor`, not sure why this +// isn't part of the standard library +template +struct overload : Ts... { + using Ts::operator()...; +}; +template +overload(Ts...) -> overload; + /** * Return the path to the directory for story temporary files. This will be * `$XDG_RUNTIME_DIR` if set, and `/tmp` otherwise. diff --git a/src/common/vst24.h b/src/common/vst24.h index 3fb38394..1ed5ed44 100644 --- a/src/common/vst24.h +++ b/src/common/vst24.h @@ -28,8 +28,8 @@ * parameter. Finally the plugin returns a string containing the input or output * name. */ -constexpr int effGetInputProperties = 33; -constexpr int effGetOutputProperties = 34; +[[maybe_unused]] constexpr int effGetInputProperties = 33; +[[maybe_unused]] constexpr int effGetOutputProperties = 34; /** * Found on @@ -37,15 +37,15 @@ constexpr int effGetOutputProperties = 34; * Used to assign names to MIDI keys, for some reason uses the `VstMidiKeyName` * struct defined below rather than a simple string. */ -constexpr int effGetMidiKeyName = 66; +[[maybe_unused]] constexpr int effGetMidiKeyName = 66; /** * Events used to tell a plugin to use a specific speaker arrangement (is this * used outside of things like Dolby Atmos?), or to query its preferred speaker * arrangement. Found on the same list as above. */ -constexpr int effSetSpeakerArrangement = 42; -constexpr int effGetSpeakerArrangement = 69; +[[maybe_unused]] constexpr int effSetSpeakerArrangement = 42; +[[maybe_unused]] constexpr int effGetSpeakerArrangement = 69; /** * The struct that's being passed through the data parameter during the diff --git a/src/plugin/bridges/common.h b/src/plugin/bridges/common.h new file mode 100644 index 00000000..cc08ecae --- /dev/null +++ b/src/plugin/bridges/common.h @@ -0,0 +1,311 @@ +// 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 . + +#pragma once + +// Generated inside of the build directory +#include +#include + +#include "../../common/configuration.h" +#include "../../common/utils.h" +#include "../host-process.h" + +/** + * Handles all common operations for hosting plugins such as initializing up the + * plugin host process, setting up the logger, and logging debug information on + * startup. + * + * @tparam Sockets the `Sockets` implementation to use. We have to initialize it + * here because we need to pass it to our `HostProcess`. + */ +template TSockets> +class PluginBridge { + public: + /** + * Sets up everything needed to start the host process. Classes deriving + * from this should call `log_init_message()` and + * `connect_sockets_guarded()` themselves after their initialization list. + * + * @param plugin_type The type of the plugin we're handling. + * @param plugin_path The path to the plugin. For VST2 plugins this is the + * path to the `.dll` file, and for VST3 plugins this is the path to the + * module (either a `.vst3` DLL file or a bundle). + * @param create_socket_instance A function to create a socket instance. + * Using a lambda here feels wrong, but I can't think of a better + * solution right now. + * + * @tparam F A `TSockets(boost::asio::io_context&, const PluginInfo&)` + * function to create the `TSockets` instance. + * + * @throw std::runtime_error Thrown when the Wine plugin host could not be + * found, or if it could not locate and load a VST3 module. + */ + template + PluginBridge(PluginType plugin_type, F create_socket_instance) + : info(plugin_type), + io_context(), + sockets(create_socket_instance(io_context, info)), + // This is still correct for VST3 plugins because we can configure an + // entire directory (the module's bundle) at once + config(load_config_for(info.native_library_path)), + generic_logger(Logger::create_from_environment( + create_logger_prefix(sockets.base_dir))), + plugin_host( + config.group + ? std::unique_ptr(std::make_unique( + io_context, + generic_logger, + info, + HostRequest{ + .plugin_type = plugin_type, + .plugin_path = info.windows_plugin_path.string(), + .endpoint_base_dir = sockets.base_dir.string()}, + sockets, + *config.group)) + : std::unique_ptr( + std::make_unique( + io_context, + generic_logger, + info, + HostRequest{.plugin_type = plugin_type, + .plugin_path = + info.windows_plugin_path.string(), + .endpoint_base_dir = + sockets.base_dir.string()}))), + has_realtime_priority(set_realtime_priority()), + wine_io_handler([&]() { io_context.run(); }) {} + + virtual ~PluginBridge(){}; + + protected: + /** + * Format and log all relevant debug information during initialization. + */ + void log_init_message() { + std::stringstream init_msg; + + init_msg << "Initializing yabridge version " << yabridge_git_version + << std::endl; + init_msg << "host: '" << plugin_host->path().string() << "'" + << std::endl; + init_msg << "plugin: '" << info.windows_plugin_path.string() + << "'" << std::endl; + init_msg << "plugin type: '" << plugin_type_to_string(info.plugin_type) + << "'" << std::endl; + init_msg << "realtime: '" << (has_realtime_priority ? "yes" : "no") + << "'" << std::endl; + init_msg << "sockets: '" << sockets.base_dir.string() << "'" + << std::endl; + init_msg << "wine prefix: '"; + + std::visit( + overload{ + [&](const OverridenWinePrefix& prefix) { + init_msg << prefix.value.string() << " "; + }, + [&](const boost::filesystem::path& prefix) { + init_msg << prefix.string(); + }, + [&](const DefaultWinePrefix&) { init_msg << ""; }, + }, + info.wine_prefix); + init_msg << "'" << std::endl; + + init_msg << "wine version: '" << get_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("").string() << "'" + << std::endl; + + init_msg << "hosting mode: '"; + if (config.group) { + init_msg << "plugin group \"" << *config.group << "\""; + } else { + init_msg << "individually"; + } + switch (info.plugin_arch) { + case LibArchitecture::dll_32: + init_msg << ", 32-bit"; + break; + case LibArchitecture::dll_64: + init_msg << ", 64-bit"; + break; + } + init_msg << "'" << std::endl; + + init_msg << "other options: "; + std::vector other_options; + if (config.cache_time_info) { + other_options.push_back("hack: time info cache"); + } + if (config.editor_double_embed) { + other_options.push_back("editor: double embed"); + } + if (config.editor_xembed) { + other_options.push_back("editor: XEmbed"); + } + if (!other_options.empty()) { + init_msg << join_quoted_strings(other_options) << std::endl; + } else { + init_msg << "''" << std::endl; + } + + // To make debugging easier, we'll print both unrecognized options (that + // might be left over when an option gets removed) as well as options + // have the wrong argument types + if (!config.invalid_options.empty()) { + init_msg << "invalid arguments: " + << join_quoted_strings(config.invalid_options) + << " (check the readme for more information)" << std::endl; + } + if (!config.unknown_options.empty()) { + init_msg << "unrecognized options: " + << join_quoted_strings(config.unknown_options) + << 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 WITH_BITBRIDGE + init_msg << "- bitbridge support" << std::endl; +#endif +#ifdef WITH_WINEDBG + init_msg << "- winedbg" << std::endl; +#endif +#ifdef WITH_VST3 + init_msg << "- VST3 support" << std::endl; +#endif +#if !(defined(WITH_BITBRIDGE) || defined(WITH_WINEDBG) || defined(WITH_VST3)) + init_msg << " " << std::endl; +#endif + init_msg << std::endl; + + for (std::string line = ""; std::getline(init_msg, line);) { + generic_logger.log(line); + } + } + + /** + * Connect the sockets, while starting another thread that will terminate + * the plugin (through `std::terminate`/SIGABRT) when the host process fails + * to start. This is the only way to stop listening on our sockets without + * moving everything over to asynchronous listeners (which may actually be a + * good idea just for this use case). Otherwise the plugin would be stuck + * loading indefinitely when Wine is not configured correctly. + * + * TODO: Asynchronously connect our sockets so we can interrupt it, maybe + */ + void connect_sockets_guarded() { +#ifndef WITH_WINEDBG + // If the Wine process fails to start, then nothing will connect to the + // sockets and we'll be hanging here indefinitely. To prevent this, + // we'll periodically poll whether the Wine process is still running, + // and throw when it is not. The alternative would be to rewrite this to + // using `async_accept`, Boost.Asio timers, and another IO context, but + // I feel like this a much simpler solution. + host_guard_handler = std::jthread([&](std::stop_token st) { + using namespace std::literals::chrono_literals; + + while (!st.stop_requested()) { + if (!plugin_host->running()) { + generic_logger.log( + "The Wine host process has exited unexpectedly. Check " + "the output above for more information."); + std::terminate(); + } + + std::this_thread::sleep_for(20ms); + } + }); +#endif + + sockets.connect(); +#ifndef WITH_WINEDBG + host_guard_handler.request_stop(); +#endif + } + + /** + * Information about the plugin we're bridging. + */ + const PluginInfo info; + + boost::asio::io_context io_context; + + /** + * The sockets used for communication with the Wine process. + * + * @remark `sockets.connect()` should not be called directly. + * `connect_sockets_guarded()` should be used instead. + * + * @see PluginBridge::connect_sockets_guarded + */ + TSockets sockets; + + /** + * The configuration for this instance of yabridge. Set based on the values + * from a `yabridge.toml`, if it exists. + * + * @see ../utils.h:load_config_for + */ + Configuration config; + + /** + * 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 generic_logger; + + /** + * The Wine process hosting our plugins. In the case of group hosts a + * `PluginBridge` instance doesn't actually own a process, but rather either + * spawns a new detached process or it connects to an existing one. + */ + std::unique_ptr plugin_host; + + /** + * Whether this process runs with realtime priority. We'll set this _after_ + * spawning the Wine process because from my testing running wineserver with + * realtime priority can actually increase latency. + */ + bool has_realtime_priority; + + /** + * Runs the Boost.Asio `io_context` thread for logging the Wine process + * STDOUT and STDERR messages. + */ + std::jthread wine_io_handler; + + private: + /** + * A thread used during the initialisation process to terminate listening on + * the sockets if the Wine process cannot start for whatever reason. This + * has to be defined here instead of in the constructor we can't simply + * detach the thread as it has to check whether the VST host is still + * running. + */ + std::jthread host_guard_handler; +}; diff --git a/src/plugin/plugin-bridge.cpp b/src/plugin/bridges/vst2.cpp similarity index 74% rename from src/plugin/plugin-bridge.cpp rename to src/plugin/bridges/vst2.cpp index 44ca3489..842f3b2f 100644 --- a/src/plugin/plugin-bridge.cpp +++ b/src/plugin/bridges/vst2.cpp @@ -14,20 +14,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include "plugin-bridge.h" +#include "vst2.h" -// Generated inside of the build directory -#include -#include - -#include "../common/communication.h" -#include "../common/utils.h" -#include "utils.h" - -namespace bp = boost::process; -// I'd rather use std::filesystem instead, but Boost.Process depends on -// boost::filesystem -namespace fs = boost::filesystem; +#include "../../common/communication/vst2.h" +#include "../utils.h" intptr_t dispatch_proxy(AEffect*, int, int, intptr_t, void*, float); void process_proxy(AEffect*, float**, float**, int); @@ -41,71 +31,32 @@ float get_parameter_proxy(AEffect*, int); * is sadly needed as a workaround to avoid using globals since we need free * function pointers to interface with the VST C API. */ -PluginBridge& get_bridge_instance(const AEffect& plugin) { - return *static_cast(plugin.ptr3); +Vst2PluginBridge& get_bridge_instance(const AEffect& plugin) { + return *static_cast(plugin.ptr3); } -PluginBridge::PluginBridge(audioMasterCallback host_callback) - : config(load_config_for(get_this_file_location())), - vst_plugin_path(find_vst_plugin()), +Vst2PluginBridge::Vst2PluginBridge(audioMasterCallback host_callback) + : PluginBridge( + PluginType::vst2, + [](boost::asio::io_context& io_context, const PluginInfo& info) { + return Vst2Sockets( + io_context, + generate_endpoint_base(info.native_library_path.filename() + .replace_extension("") + .string()), + true); + }), // All the fields should be zero initialized because // `Vst2PluginInstance::vstAudioMasterCallback` from Bitwig's plugin // bridge will crash otherwise plugin(), - io_context(), - sockets(io_context, - generate_endpoint_base( - vst_plugin_path.filename().replace_extension("").string()), - true), host_callback_function(host_callback), - logger(Logger::create_from_environment( - create_logger_prefix(sockets.base_dir))), - wine_version(get_wine_version()), - vst_host(config.group - ? std::unique_ptr( - std::make_unique(io_context, - logger, - vst_plugin_path, - sockets, - *config.group)) - : std::unique_ptr( - std::make_unique(io_context, - logger, - vst_plugin_path, - sockets))), - has_realtime_priority(set_realtime_priority()), - wine_io_handler([&]() { io_context.run(); }) { + logger(generic_logger) { log_init_message(); -#ifndef WITH_WINEDBG - // If the Wine process fails to start, then nothing will connect to the - // sockets and we'll be hanging here indefinitely. To prevent this, we'll - // periodically poll whether the Wine process is still running, and throw - // when it is not. The alternative would be to rewrite this to using - // `async_accept`, Boost.Asio timers, and another IO context, but I feel - // like this a much simpler solution. - host_guard_handler = std::jthread([&](std::stop_token st) { - using namespace std::literals::chrono_literals; - - while (!st.stop_requested()) { - if (!vst_host->running()) { - logger.log( - "The Wine host process has exited unexpectedly. Check the " - "output above for more information."); - std::terminate(); - } - - std::this_thread::sleep_for(20ms); - } - }); -#endif - // This will block until all sockets have been connected to by the Wine VST // host - sockets.connect(); -#ifndef WITH_WINEDBG - host_guard_handler.request_stop(); -#endif + connect_sockets_guarded(); // Set up all pointers for our `AEffect` struct. We will fill this with data // from the VST plugin loaded in Wine at the end of this constructor. @@ -122,7 +73,7 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback) // lockstep anyway host_callback_handler = std::jthread([&]() { sockets.vst_host_callback.receive_events( - std::pair(logger, false), + std::pair(logger, false), [&](Event& event, bool /*on_main_thread*/) { // MIDI events sent from the plugin back to the host are a // special case here. They have to sent during the @@ -164,6 +115,15 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback) update_aeffect(plugin, initialized_plugin); } +Vst2PluginBridge::~Vst2PluginBridge() { + // Drop all work make sure all sockets are closed + plugin_host->terminate(); + + // The `stop()` method will cause the IO context to just drop all of its + // outstanding work immediately + io_context.stop(); +} + class DispatchDataConverter : DefaultDataConverter { public: DispatchDataConverter(std::vector& chunk_data, @@ -297,8 +257,8 @@ class DispatchDataConverter : DefaultDataConverter { } break; case effGetChunk: { // Write the chunk data to some publically accessible place in - // `PluginBridge` and write a pointer to that struct to the data - // pointer + // `Vst2PluginBridge` and write a pointer to that struct to the + // data pointer const auto buffer = std::get(response.payload).buffer; chunk.assign(buffer.begin(), buffer.end()); @@ -391,12 +351,12 @@ class DispatchDataConverter : DefaultDataConverter { VstRect& rect; }; -intptr_t PluginBridge::dispatch(AEffect* /*plugin*/, - int opcode, - int index, - intptr_t value, - void* data, - float option) { +intptr_t Vst2PluginBridge::dispatch(AEffect* /*plugin*/, + int opcode, + int index, + intptr_t value, + void* data, + float option) { // HACK: Ardour 5.X has a bug in its VST implementation where it calls the // plugin's dispatcher before the plugin has even finished // initializing. This has been fixed back in 2018, but there has not @@ -426,21 +386,14 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/, try { // TODO: Add some kind of timeout? return_value = sockets.host_vst_dispatch.send_event( - converter, std::pair(logger, true), opcode, - index, value, data, option); + converter, std::pair(logger, true), + opcode, index, value, data, option); } catch (const boost::system::system_error& a) { // Thrown when the socket gets closed because the VST plugin // loaded into the Wine process crashed during shutdown logger.log("The plugin crashed during shutdown, ignoring"); } - vst_host->terminate(); - - // The `stop()` method will cause the IO context to just drop all of - // its work immediately and not throw any exceptions that would have - // been caused by pipes and sockets being closed. - io_context.stop(); - delete this; return return_value; @@ -478,19 +431,19 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/, // receiving function temporarily allocate a large enough buffer rather than // to have a bunch of allocated memory sitting around doing nothing. return sockets.host_vst_dispatch.send_event( - converter, std::pair(logger, true), opcode, index, value, - data, option); + converter, std::pair(logger, true), opcode, index, + value, data, option); } template -void PluginBridge::do_process(T** inputs, T** outputs, int sample_frames) { +void Vst2PluginBridge::do_process(T** inputs, T** outputs, int sample_frames) { // The inputs and outputs arrays should be `[num_inputs][sample_frames]` and // `[num_outputs][sample_frames]` floats large respectfully. std::vector> input_buffers(plugin.numInputs, std::vector(sample_frames)); for (int channel = 0; channel < plugin.numInputs; channel++) { - std::copy(inputs[channel], inputs[channel] + sample_frames, - input_buffers[channel].begin()); + std::copy_n(inputs[channel], sample_frames, + input_buffers[channel].begin()); } const AudioBuffers request{input_buffers, sample_frames}; @@ -541,10 +494,10 @@ void PluginBridge::do_process(T** inputs, T** outputs, int sample_frames) { incoming_midi_events.clear(); } -void PluginBridge::process(AEffect* /*plugin*/, - float** inputs, - float** outputs, - int sample_frames) { +void Vst2PluginBridge::process(AEffect* /*plugin*/, + float** inputs, + float** outputs, + int sample_frames) { // Technically either `Vst2PluginBridge::process()` or // `Vst2PluginBridge::process_replacing()` could actually call the other // function on the plugin depending on what the plugin supports. @@ -553,25 +506,25 @@ void PluginBridge::process(AEffect* /*plugin*/, logger.log_trace(" process() :: end"); } -void PluginBridge::process_replacing(AEffect* /*plugin*/, - float** inputs, - float** outputs, - int sample_frames) { +void Vst2PluginBridge::process_replacing(AEffect* /*plugin*/, + float** inputs, + float** outputs, + int sample_frames) { logger.log_trace(">> processReplacing() :: start"); do_process(inputs, outputs, sample_frames); logger.log_trace(" processReplacing() :: end"); } -void PluginBridge::process_double_replacing(AEffect* /*plugin*/, - double** inputs, - double** outputs, - int sample_frames) { +void Vst2PluginBridge::process_double_replacing(AEffect* /*plugin*/, + double** inputs, + double** outputs, + int sample_frames) { logger.log_trace(">> processDoubleReplacing() :: start"); do_process(inputs, outputs, sample_frames); logger.log_trace(" processDoubleReplacing() :: end"); } -float PluginBridge::get_parameter(AEffect* /*plugin*/, int index) { +float Vst2PluginBridge::get_parameter(AEffect* /*plugin*/, int index) { logger.log_get_parameter(index); const Parameter request{index, std::nullopt}; @@ -592,7 +545,9 @@ float PluginBridge::get_parameter(AEffect* /*plugin*/, int index) { return *response.value; } -void PluginBridge::set_parameter(AEffect* /*plugin*/, int index, float value) { +void Vst2PluginBridge::set_parameter(AEffect* /*plugin*/, + int index, + float value) { logger.log_set_parameter(index, value); const Parameter request{index, value}; @@ -612,102 +567,6 @@ void PluginBridge::set_parameter(AEffect* /*plugin*/, int index, float value) { assert(!response.value); } -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 << "realtime: '" << (has_realtime_priority ? "yes" : "no") - << "'" << std::endl; - init_msg << "sockets: '" << sockets.base_dir.string() << "'" - << std::endl; - init_msg << "wine prefix: '"; - - // If the Wine prefix is manually overridden, then this should be made - // clear. This follows the behaviour of `set_wineprefix()`. - bp::environment env = boost::this_process::environment(); - if (!env["WINEPREFIX"].empty()) { - init_msg << env["WINEPREFIX"].to_string() << " "; - } else { - init_msg << find_wineprefix().value_or("").string(); - } - init_msg << "'" << 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("").string() << "'" - << std::endl; - - init_msg << "hosting mode: '"; - if (config.group) { - init_msg << "plugin group \"" << *config.group << "\""; - } else { - init_msg << "individually"; - } - if (vst_host->architecture() == PluginArchitecture::vst_32) { - init_msg << ", 32-bit"; - } else { - init_msg << ", 64-bit"; - } - init_msg << "'" << std::endl; - - init_msg << "other options: "; - std::vector other_options; - if (config.cache_time_info) { - other_options.push_back("hack: time info cache"); - } - if (config.editor_double_embed) { - other_options.push_back("editor: double embed"); - } - if (!other_options.empty()) { - init_msg << join_quoted_strings(other_options) << std::endl; - } else { - init_msg << "''" << std::endl; - } - - // To make debugging easier, we'll print both unrecognized options (that - // might be left over when an option gets removed) as well as options have - // the wrong argument types - if (!config.invalid_options.empty()) { - init_msg << "invalid arguments: " - << join_quoted_strings(config.invalid_options) - << " (check the readme for more information)" << std::endl; - } - if (!config.unknown_options.empty()) { - init_msg << "unrecognized options: " - << join_quoted_strings(config.unknown_options) << 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 WITH_BITBRIDGE - init_msg << "- bitbridge support" << std::endl; -#endif -#ifdef WITH_WINEDBG - init_msg << "- winedbg" << std::endl; -#endif -#if !(defined(WITH_BITBRIDGE) || defined(WITH_WINEDBG)) - init_msg << " " << 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 // `Bridge.cpp` diff --git a/src/plugin/plugin-bridge.h b/src/plugin/bridges/vst2.h similarity index 71% rename from src/plugin/plugin-bridge.h rename to src/plugin/bridges/vst2.h index c9da720f..a363830d 100644 --- a/src/plugin/plugin-bridge.h +++ b/src/plugin/bridges/vst2.h @@ -19,21 +19,22 @@ #include #include -#include -#include #include -#include "../common/communication.h" -#include "../common/configuration.h" -#include "../common/logging.h" -#include "host-process.h" +#include "../../common/communication/vst2.h" +#include "../../common/logging/vst2.h" +#include "common.h" /** - * This handles the communication between the Linux native VST plugin and the + * This handles the communication between the Linux native VST2 plugin and the * Wine VST host. The functions below should be used as callback functions in an * `AEffect` object. + * + * The naming scheme of all of these 'bridge' classes is `{,Plugin}Bridge` + * for greppability reasons. The `Plugin` infix is added on the native plugin + * side. */ -class PluginBridge { +class Vst2PluginBridge : PluginBridge> { public: /** * Initializes the Wine VST bridge. This sets up the sockets for event @@ -45,7 +46,13 @@ class PluginBridge { * @throw std::runtime_error Thrown when the VST host could not be found, or * if it could not locate and load a VST .dll file. */ - PluginBridge(audioMasterCallback host_callback); + Vst2PluginBridge(audioMasterCallback host_callback); + + /** + * Terminate the Wine plugin host process and drop all work when the module + * gets unloaded. + */ + ~Vst2PluginBridge(); // The four below functions are the handlers from the VST2 API. They are // called through proxy functions in `plugin.cpp`. @@ -83,10 +90,10 @@ class PluginBridge { float** outputs, int sample_frames); /** - * The same as `PluginBridge::process_replacing`, but for double precision - * audio. Support for this on both the plugin and host side is pretty rare, - * but REAPER supports it. This reuses the same infrastructure as - * `process_replacing` is using since the host will only call one or the + * The same as `Vst2PluginBridge::process_replacing`, but for double + * precision audio. Support for this on both the plugin and host side is + * pretty rare, but REAPER supports it. This reuses the same infrastructure + * as `process_replacing` is using since the host will only call one or the * other. */ void process_double_replacing(AEffect* plugin, @@ -108,25 +115,12 @@ class PluginBridge { * values in `outputs`. No host will use this last behaviour anymore, but * it's part of the VST2.4 spec so we have to support it. * - * @see PluginBridge::process_replacing - * @see PluginBridge::process_double_replacing + * @see Vst2PluginBridge::process_replacing + * @see Vst2PluginBridge::process_double_replacing */ template void do_process(T** inputs, T** outputs, int sample_frames); - /** - * The configuration for this instance of yabridge. Set based on the values - * from a `yabridge.toml`, if it exists. - * - * @see ./utils.h:load_config_for - */ - Configuration config; - - /** - * The path to the .dll being loaded in the Wine VST host. - */ - const boost::filesystem::path vst_plugin_path; - /** * This AEffect struct will be populated using the data passed by the Wine * VST host during initialization and then passed as a pointer to the Linux @@ -135,14 +129,6 @@ class PluginBridge { AEffect plugin; private: - /** - * Format and log all relevant debug information during initialization. - */ - void log_init_message(); - - boost::asio::io_context io_context; - Sockets sockets; - /** * The thread that handles host callbacks. */ @@ -162,47 +148,10 @@ 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 + * The logging facility used for this instance of yabridge. Wraps around + * `PluginBridge::generic_logger`. */ - Logger logger; - - /** - * The version of Wine currently in use. Used in the debug output on plugin - * startup. - */ - const std::string wine_version; - - /** - * The Wine process hosting the Windows VST plugin. - * - * @see launch_vst_host - */ - std::unique_ptr vst_host; - - /** - * A thread used during the initialisation process to terminate listening on - * the sockets if the Wine process cannot start for whatever reason. This - * has to be defined here instead of in the constructor we can't simply - * detach the thread as it has to check whether the VST host is still - * running. - */ - std::jthread host_guard_handler; - - /** - * Whether this process runs with realtime priority. We'll set this _after_ - * spawning the Wine process because from my testing running wineserver with - * realtime priority can actually increase latency. - */ - bool has_realtime_priority; - - /** - * Runs the Boost.Asio `io_context` thread for logging the Wine process - * STDOUT and STDERR messages. - */ - std::jthread wine_io_handler; + Vst2Logger logger; /** * A scratch buffer for sending and receiving data during `process`, diff --git a/src/plugin/bridges/vst3-impls/plug-view-proxy.cpp b/src/plugin/bridges/vst3-impls/plug-view-proxy.cpp new file mode 100644 index 00000000..3485b73e --- /dev/null +++ b/src/plugin/bridges/vst3-impls/plug-view-proxy.cpp @@ -0,0 +1,157 @@ +// 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 . + +#include "plug-view-proxy.h" + +Vst3PlugViewProxyImpl::Vst3PlugViewProxyImpl( + Vst3PluginBridge& bridge, + Vst3PlugViewProxy::ConstructArgs&& args) + : Vst3PlugViewProxy(std::move(args)), bridge(bridge) {} + +Vst3PlugViewProxyImpl::~Vst3PlugViewProxyImpl() { + // Also drop the plug view smart pointer on the Wine side when this gets + // dropped + bridge.send_message( + Vst3PlugViewProxy::Destruct{.owner_instance_id = owner_instance_id()}); +} + +tresult PLUGIN_API +Vst3PlugViewProxyImpl::queryInterface(const Steinberg::TUID _iid, void** obj) { + // TODO: Successful queries should also be logged + const tresult result = Vst3PlugViewProxy::queryInterface(_iid, obj); + if (result != Steinberg::kResultOk) { + bridge.logger.log_unknown_interface("In IPlugView::queryInterface()", + Steinberg::FUID::fromTUID(_iid)); + } + + return result; +} + +tresult PLUGIN_API +Vst3PlugViewProxyImpl::isPlatformTypeSupported(Steinberg::FIDString type) { + // We'll swap the X11 window ID platform type string for the Win32 HWND + // equivalent on the Wine side + return bridge.send_message(YaPlugView::IsPlatformTypeSupported{ + .owner_instance_id = owner_instance_id(), .type = type}); +} + +tresult PLUGIN_API Vst3PlugViewProxyImpl::attached(void* parent, + Steinberg::FIDString type) { + // We will embed the Wine Win32 window into the X11 window provided by the + // host + return bridge.send_message( + YaPlugView::Attached{.owner_instance_id = owner_instance_id(), + .parent = reinterpret_cast(parent), + .type = type}); +} + +tresult PLUGIN_API Vst3PlugViewProxyImpl::removed() { + return bridge.send_message( + YaPlugView::Removed{.owner_instance_id = owner_instance_id()}); +} + +tresult PLUGIN_API Vst3PlugViewProxyImpl::onWheel(float distance) { + return bridge.send_message(YaPlugView::OnWheel{ + .owner_instance_id = owner_instance_id(), .distance = distance}); +} + +tresult PLUGIN_API Vst3PlugViewProxyImpl::onKeyDown(char16 key, + int16 keyCode, + int16 modifiers) { + return bridge.send_message( + YaPlugView::OnKeyDown{.owner_instance_id = owner_instance_id(), + .key = key, + .key_code = keyCode, + .modifiers = modifiers}); +} + +tresult PLUGIN_API Vst3PlugViewProxyImpl::onKeyUp(char16 key, + int16 keyCode, + int16 modifiers) { + return bridge.send_message( + YaPlugView::OnKeyUp{.owner_instance_id = owner_instance_id(), + .key = key, + .key_code = keyCode, + .modifiers = modifiers}); +} + +tresult PLUGIN_API Vst3PlugViewProxyImpl::getSize(Steinberg::ViewRect* size) { + if (size) { + const GetSizeResponse response = + bridge.send_message(YaPlugView::GetSize{ + .owner_instance_id = owner_instance_id(), .size = *size}); + + *size = response.updated_size; + + return response.result; + } else { + bridge.logger.log( + "WARNING: Null pointer passed to 'IPlugView::getSize()'"); + return Steinberg::kInvalidArgument; + } +} + +tresult PLUGIN_API Vst3PlugViewProxyImpl::onSize(Steinberg::ViewRect* newSize) { + if (newSize) { + return bridge.send_message(YaPlugView::OnSize{ + .owner_instance_id = owner_instance_id(), .new_size = *newSize}); + } else { + bridge.logger.log( + "WARNING: Null pointer passed to 'IPlugView::onSize()'"); + return Steinberg::kInvalidArgument; + } +} + +tresult PLUGIN_API Vst3PlugViewProxyImpl::onFocus(TBool state) { + return bridge.send_message(YaPlugView::OnFocus{ + .owner_instance_id = owner_instance_id(), .state = state}); +} + +tresult PLUGIN_API +Vst3PlugViewProxyImpl::setFrame(Steinberg::IPlugFrame* frame) { + if (frame) { + // We'll store the pointer for when the plugin later makes a callback to + // this component handler + plug_frame = frame; + + return bridge.send_message(YaPlugView::SetFrame{ + .owner_instance_id = owner_instance_id(), + .plug_frame_args = Vst3PlugFrameProxy::ConstructArgs( + plug_frame, owner_instance_id())}); + } else { + bridge.logger.log( + "WARNING: Null pointer passed to 'IPlugView::setFrame()'"); + return Steinberg::kInvalidArgument; + } +} + +tresult PLUGIN_API Vst3PlugViewProxyImpl::canResize() { + return bridge.send_message( + YaPlugView::CanResize{.owner_instance_id = owner_instance_id()}); +} + +tresult PLUGIN_API +Vst3PlugViewProxyImpl::checkSizeConstraint(Steinberg::ViewRect* rect) { + if (rect) { + return bridge.send_message(YaPlugView::CheckSizeConstraint{ + .owner_instance_id = owner_instance_id(), .rect = *rect}); + } else { + bridge.logger.log( + "WARNING: Null pointer passed to " + "'IPlugView::checkSizeConstraint()'"); + return Steinberg::kInvalidArgument; + } +} diff --git a/src/plugin/bridges/vst3-impls/plug-view-proxy.h b/src/plugin/bridges/vst3-impls/plug-view-proxy.h new file mode 100644 index 00000000..0a9711f0 --- /dev/null +++ b/src/plugin/bridges/vst3-impls/plug-view-proxy.h @@ -0,0 +1,69 @@ +// 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 . + +#pragma once + +#include "../vst3.h" + +class Vst3PlugViewProxyImpl : public Vst3PlugViewProxy { + public: + Vst3PlugViewProxyImpl(Vst3PluginBridge& bridge, + Vst3PlugViewProxy::ConstructArgs&& args); + + /** + * When the reference count reaches zero and this destructor is called, + * we'll send a request to the Wine plugin host to destroy the corresponding + * object. + */ + ~Vst3PlugViewProxyImpl(); + + /** + * We'll override the query interface to log queries for interfaces we do + * not (yet) support. + */ + tresult PLUGIN_API queryInterface(const Steinberg::TUID _iid, + void** obj) override; + + // From `IPlugView` + tresult PLUGIN_API + isPlatformTypeSupported(Steinberg::FIDString type) override; + tresult PLUGIN_API attached(void* parent, + Steinberg::FIDString type) override; + tresult PLUGIN_API removed() override; + tresult PLUGIN_API onWheel(float distance) override; + tresult PLUGIN_API onKeyDown(char16 key, + int16 keyCode, + int16 modifiers) override; + tresult PLUGIN_API onKeyUp(char16 key, + int16 keyCode, + int16 modifiers) override; + tresult PLUGIN_API getSize(Steinberg::ViewRect* size) override; + tresult PLUGIN_API onSize(Steinberg::ViewRect* newSize) override; + tresult PLUGIN_API onFocus(TBool state) override; + tresult PLUGIN_API setFrame(Steinberg::IPlugFrame* frame) override; + tresult PLUGIN_API canResize() override; + tresult PLUGIN_API checkSizeConstraint(Steinberg::ViewRect* rect) override; + + /** + * The `IPlugFrame` object passed by the host passed to us in + * `IPlugView::setFrame()`. When the plugin makes a callback on the + * `IPlugFrame` proxy object, we'll pass the call through to this object. + */ + Steinberg::IPtr plug_frame; + + private: + Vst3PluginBridge& bridge; +}; diff --git a/src/plugin/bridges/vst3-impls/plugin-factory.cpp b/src/plugin/bridges/vst3-impls/plugin-factory.cpp new file mode 100644 index 00000000..ac0b4ca9 --- /dev/null +++ b/src/plugin/bridges/vst3-impls/plugin-factory.cpp @@ -0,0 +1,124 @@ +// 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 . + +#include "plugin-factory.h" + +#include + +#include "plugin-proxy.h" + +YaPluginFactoryImpl::YaPluginFactoryImpl(Vst3PluginBridge& bridge, + YaPluginFactory::ConstructArgs&& args) + : YaPluginFactory(std::move(args)), bridge(bridge) {} + +tresult PLUGIN_API +YaPluginFactoryImpl::createInstance(Steinberg::FIDString cid, + Steinberg::FIDString _iid, + void** obj) { + // Class IDs may be padded with null bytes + constexpr size_t uid_size = sizeof(Steinberg::TUID); + if (!cid || !_iid || !obj || strnlen(_iid, uid_size) < uid_size) { + return Steinberg::kInvalidArgument; + } + + ArrayUID cid_array; + std::copy(cid, cid + std::extent_v, cid_array.begin()); + + // FIXME: `_iid` in Bitwig Studio 3.3.1 is not null terminated, and the + // comparison below will thus fail since the strings have different + // lengths. Since it looks like the module implementation that comes + // with the SDK has this same issue I think it might just be a case + // of Steinberg not following its own specifications. + std::string iid_string(_iid, uid_size); + + Vst3PluginProxy::Construct::Interface requested_interface; + if (Steinberg::FIDStringsEqual(iid_string.c_str(), + Steinberg::Vst::IComponent::iid)) { + requested_interface = Vst3PluginProxy::Construct::Interface::IComponent; + } else if (Steinberg::FIDStringsEqual( + iid_string.c_str(), Steinberg::Vst::IEditController::iid)) { + requested_interface = + Vst3PluginProxy::Construct::Interface::IEditController; + } else { + // When the host requests an interface we do not (yet) implement, we'll + // print a recognizable log message. I don't think they include a safe + // way to convert a `FIDString/char*` into a `FUID`, so this will have + // to do. + const Steinberg::FUID uid = Steinberg::FUID::fromTUID( + *reinterpret_cast(&*_iid)); + bridge.logger.log_unknown_interface( + "In IPluginFactory::createInstance()", uid); + + *obj = nullptr; + return Steinberg::kNotImplemented; + } + + std::variant result = + bridge.send_message(Vst3PluginProxy::Construct{ + .cid = cid_array, .requested_interface = requested_interface}); + + return std::visit( + overload{ + [&](Vst3PluginProxy::ConstructArgs&& args) -> tresult { + // These pointers are scary. The idea here is that we return a + // newly initialized object (that initializes itself with a + // reference count of 1), and then the receiving side will use + // `Steinberg::owned()` to adopt it to an `IPtr`. + Vst3PluginProxyImpl* proxy_object = + new Vst3PluginProxyImpl(bridge, std::move(args)); + + // We return a properly downcasted version of the proxy object + // we just created + switch (requested_interface) { + case Vst3PluginProxy::Construct::Interface::IComponent: + *obj = static_cast( + proxy_object); + break; + case Vst3PluginProxy::Construct::Interface::IEditController: + *obj = static_cast( + proxy_object); + break; + } + + return Steinberg::kResultOk; + }, + [&](const UniversalTResult& code) -> tresult { return code; }}, + std::move(result)); +} + +tresult PLUGIN_API +YaPluginFactoryImpl::setHostContext(Steinberg::FUnknown* context) { + if (context) { + // We will create a proxy object that that supports all the same + // interfaces as `context`, and then we'll store `context` in this + // object. We can then use it to handle callbacks made by the Windows + // VST3 plugin to this context. + host_context = context; + + // Automatically converted smart pointers for when the plugin performs a + // callback later + host_application = host_context; + + return bridge.send_message(YaPluginFactory::SetHostContext{ + .host_context_args = Vst3HostContextProxy::ConstructArgs( + host_context, std::nullopt)}); + } else { + bridge.logger.log( + "WARNING: Null pointer passed to " + "'IPluginFactory3::setHostContext()'"); + return Steinberg::kInvalidArgument; + } +} diff --git a/src/plugin/bridges/vst3-impls/plugin-factory.h b/src/plugin/bridges/vst3-impls/plugin-factory.h new file mode 100644 index 00000000..209ce765 --- /dev/null +++ b/src/plugin/bridges/vst3-impls/plugin-factory.h @@ -0,0 +1,44 @@ +// 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 . + +#pragma once + +#include "../vst3.h" + +class YaPluginFactoryImpl : public YaPluginFactory { + public: + YaPluginFactoryImpl(Vst3PluginBridge& bridge, + YaPluginFactory::ConstructArgs&& args); + + tresult PLUGIN_API createInstance(Steinberg::FIDString cid, + Steinberg::FIDString _iid, + void** obj) override; + tresult PLUGIN_API setHostContext(Steinberg::FUnknown* context) override; + + // The following pointers are cast from `host_context` if + // `IPluginFactory3::setHostContext()` has been called + + Steinberg::FUnknownPtr host_application; + + private: + Vst3PluginBridge& bridge; + + /** + * An host context if we get passed one through + * `IPluginFactory3::setHostContext()`. + */ + Steinberg::IPtr host_context; +}; diff --git a/src/plugin/bridges/vst3-impls/plugin-proxy.cpp b/src/plugin/bridges/vst3-impls/plugin-proxy.cpp new file mode 100644 index 00000000..e5aca967 --- /dev/null +++ b/src/plugin/bridges/vst3-impls/plugin-proxy.cpp @@ -0,0 +1,636 @@ +// 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 . + +#include "plugin-proxy.h" + +#include "plug-view-proxy.h" + +Vst3PluginProxyImpl::Vst3PluginProxyImpl(Vst3PluginBridge& bridge, + Vst3PluginProxy::ConstructArgs&& args) + : Vst3PluginProxy(std::move(args)), bridge(bridge) { + bridge.register_plugin_proxy(*this); +} + +Vst3PluginProxyImpl::~Vst3PluginProxyImpl() { + bridge.send_message( + Vst3PluginProxy::Destruct{.instance_id = instance_id()}); + bridge.unregister_plugin_proxy(*this); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::queryInterface(const Steinberg::TUID _iid, void** obj) { + // TODO: Successful queries should also be logged + const tresult result = Vst3PluginProxy::queryInterface(_iid, obj); + if (result != Steinberg::kResultOk) { + bridge.logger.log_unknown_interface("In FUnknown::queryInterface()", + Steinberg::FUID::fromTUID(_iid)); + } + + return result; +} + +tresult PLUGIN_API Vst3PluginProxyImpl::setBusArrangements( + Steinberg::Vst::SpeakerArrangement* inputs, + int32 numIns, + Steinberg::Vst::SpeakerArrangement* outputs, + int32 numOuts) { + // NOTE: Ardour passes a null pointer when `numIns` or `numOuts` is 0, so we + // need to work around that + return bridge.send_audio_processor_message( + YaAudioProcessor::SetBusArrangements{ + .instance_id = instance_id(), + .inputs = + (inputs ? std::vector( + inputs, &inputs[numIns]) + : std::vector()), + .num_ins = numIns, + .outputs = + (outputs ? std::vector( + outputs, &outputs[numOuts]) + : std::vector()), + .num_outs = numOuts, + }); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::getBusArrangement( + Steinberg::Vst::BusDirection dir, + int32 index, + Steinberg::Vst::SpeakerArrangement& arr) { + const GetBusArrangementResponse response = + bridge.send_audio_processor_message( + YaAudioProcessor::GetBusArrangement{.instance_id = instance_id(), + .dir = dir, + .index = index, + .arr = arr}); + + arr = response.updated_arr; + + return response.result; +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::canProcessSampleSize(int32 symbolicSampleSize) { + return bridge.send_audio_processor_message( + YaAudioProcessor::CanProcessSampleSize{ + .instance_id = instance_id(), + .symbolic_sample_size = symbolicSampleSize}); +} + +uint32 PLUGIN_API Vst3PluginProxyImpl::getLatencySamples() { + return bridge.send_audio_processor_message( + YaAudioProcessor::GetLatencySamples{.instance_id = instance_id()}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::setupProcessing(Steinberg::Vst::ProcessSetup& setup) { + return bridge.send_audio_processor_message( + YaAudioProcessor::SetupProcessing{.instance_id = instance_id(), + .setup = setup}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::setProcessing(TBool state) { + return bridge.send_audio_processor_message(YaAudioProcessor::SetProcessing{ + .instance_id = instance_id(), .state = state}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::process(Steinberg::Vst::ProcessData& data) { + // TODO: Check whether reusing a `YaProcessData` object make a difference in + // terms of performance + ProcessResponse response = bridge.send_audio_processor_message( + YaAudioProcessor::Process{.instance_id = instance_id(), .data = data}); + + response.output_data.write_back_outputs(data); + + return response.result; +} + +uint32 PLUGIN_API Vst3PluginProxyImpl::getTailSamples() { + return bridge.send_audio_processor_message( + YaAudioProcessor::GetTailSamples{.instance_id = instance_id()}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::getControllerClassId(Steinberg::TUID classId) { + const GetControllerClassIdResponse response = + bridge.send_audio_processor_message( + YaComponent::GetControllerClassId{.instance_id = instance_id()}); + + std::copy(response.editor_cid.begin(), response.editor_cid.end(), classId); + + return response.result; +} + +tresult PLUGIN_API Vst3PluginProxyImpl::setIoMode(Steinberg::Vst::IoMode mode) { + return bridge.send_audio_processor_message( + YaComponent::SetIoMode{.instance_id = instance_id(), .mode = mode}); +} + +int32 PLUGIN_API +Vst3PluginProxyImpl::getBusCount(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir) { + return bridge.send_audio_processor_message(YaComponent::GetBusCount{ + .instance_id = instance_id(), .type = type, .dir = dir}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::getBusInfo(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir, + int32 index, + Steinberg::Vst::BusInfo& bus /*out*/) { + const GetBusInfoResponse response = bridge.send_audio_processor_message( + YaComponent::GetBusInfo{.instance_id = instance_id(), + .type = type, + .dir = dir, + .index = index, + .bus = bus}); + + bus = response.updated_bus; + return response.result; +} + +tresult PLUGIN_API Vst3PluginProxyImpl::getRoutingInfo( + Steinberg::Vst::RoutingInfo& inInfo, + Steinberg::Vst::RoutingInfo& outInfo /*out*/) { + const GetRoutingInfoResponse response = bridge.send_audio_processor_message( + YaComponent::GetRoutingInfo{.instance_id = instance_id(), + .in_info = inInfo, + .out_info = outInfo}); + + inInfo = response.updated_in_info; + outInfo = response.updated_out_info; + return response.result; +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::activateBus(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir, + int32 index, + TBool state) { + return bridge.send_audio_processor_message( + YaComponent::ActivateBus{.instance_id = instance_id(), + .type = type, + .dir = dir, + .index = index, + .state = state}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::setActive(TBool state) { + return bridge.send_audio_processor_message( + YaComponent::SetActive{.instance_id = instance_id(), .state = state}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::setState(Steinberg::IBStream* state) { + // Since both interfaces contain this function, this is used for both + // `IComponent::setState()` as well as `IEditController::setState()` + return bridge.send_message(Vst3PluginProxy::SetState{ + .instance_id = instance_id(), .state = state}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::getState(Steinberg::IBStream* state) { + // Since both interfaces contain this function, this is used for both + // `IComponent::getState()` as well as `IEditController::getState()` + const GetStateResponse response = bridge.send_message( + Vst3PluginProxy::GetState{.instance_id = instance_id()}); + + assert(response.updated_state.write_back(state) == Steinberg::kResultOk); + + return response.result; +} + +tresult PLUGIN_API Vst3PluginProxyImpl::connect(IConnectionPoint* other) { + // When the host is trying to connect two plugin proxy objects, we can just + // identify the other object by its instance IDs and then connect the + // objects in the Wine plugin host directly. Otherwise we'll have to set up + // a proxy for the host's connection proxy so the messages can be routed + // through that. + if (auto other_proxy = dynamic_cast(other)) { + return bridge.send_message(YaConnectionPoint::Connect{ + .instance_id = instance_id(), .other = other_proxy->instance_id()}); + } else { + connection_point_proxy = other; + + return bridge.send_message(YaConnectionPoint::Connect{ + .instance_id = instance_id(), + .other = + Vst3ConnectionPointProxy::ConstructArgs(other, instance_id())}); + } +} + +tresult PLUGIN_API Vst3PluginProxyImpl::disconnect(IConnectionPoint* other) { + // See `Vst3PluginProxyImpl::connect()` + if (auto other_proxy = dynamic_cast(other)) { + return bridge.send_message(YaConnectionPoint::Disconnect{ + .instance_id = instance_id(), + .other_instance_id = other_proxy->instance_id()}); + } else { + const tresult result = bridge.send_message( + YaConnectionPoint::Disconnect{.instance_id = instance_id(), + .other_instance_id = std::nullopt}); + connection_point_proxy.reset(); + + return result; + } +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::notify(Steinberg::Vst::IMessage* message) { + // Since there is no way to enumerate over all values in an + // `IAttributeList`, we can only support relaying messages that were sent by + // our own objects. Additionally, the `IMessage*` we end up passing to the + // plugin needs to have the same lifetime as the original object, because + // some plugins are being a bit naughty. That's why we pass around a pointer + // to the original message object. + // All of this is only needed to support hosts that place a connection proxy + // between two objects instead of connecting them directly. If the objects + // are connected directly we also connected them directly on the Wine side, + // so we don't have to do any additional when those objects pass through + // messages. + if (auto message_ptr = dynamic_cast(message)) { + return bridge.send_message(YaConnectionPoint::Notify{ + .instance_id = instance_id(), .message_ptr = *message_ptr}); + } else { + bridge.logger.log( + "WARNING: Unknown message type passed to " + "'IConnectionPoint::notify()', ignoring"); + return Steinberg::kNotImplemented; + } +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::setComponentState(Steinberg::IBStream* state) { + return bridge.send_message(YaEditController::SetComponentState{ + .instance_id = instance_id(), .state = state}); +} + +int32 PLUGIN_API Vst3PluginProxyImpl::getParameterCount() { + return bridge.send_message( + YaEditController::GetParameterCount{.instance_id = instance_id()}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::getParameterInfo( + int32 paramIndex, + Steinberg::Vst::ParameterInfo& info /*out*/) { + const GetParameterInfoResponse response = bridge.send_message( + YaEditController::GetParameterInfo{.instance_id = instance_id(), + .param_index = paramIndex, + .info = info}); + + info = response.updated_info; + + return response.result; +} + +tresult PLUGIN_API Vst3PluginProxyImpl::getParamStringByValue( + Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue valueNormalized /*in*/, + Steinberg::Vst::String128 string /*out*/) { + const GetParamStringByValueResponse response = + bridge.send_message(YaEditController::GetParamStringByValue{ + .instance_id = instance_id(), + .id = id, + .value_normalized = valueNormalized}); + + std::copy(response.string.begin(), response.string.end(), string); + string[response.string.size()] = 0; + + return response.result; +} + +tresult PLUGIN_API Vst3PluginProxyImpl::getParamValueByString( + Steinberg::Vst::ParamID id, + Steinberg::Vst::TChar* string /*in*/, + Steinberg::Vst::ParamValue& valueNormalized /*out*/) { + const GetParamValueByStringResponse response = + bridge.send_message(YaEditController::GetParamValueByString{ + .instance_id = instance_id(), .id = id, .string = string}); + + valueNormalized = response.value_normalized; + + return response.result; +} + +Steinberg::Vst::ParamValue PLUGIN_API +Vst3PluginProxyImpl::normalizedParamToPlain( + Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue valueNormalized) { + return bridge.send_message(YaEditController::NormalizedParamToPlain{ + .instance_id = instance_id(), + .id = id, + .value_normalized = valueNormalized}); +} + +Steinberg::Vst::ParamValue PLUGIN_API +Vst3PluginProxyImpl::plainParamToNormalized( + Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue plainValue) { + return bridge.send_message(YaEditController::PlainParamToNormalized{ + .instance_id = instance_id(), .id = id, .plain_value = plainValue}); +} + +Steinberg::Vst::ParamValue PLUGIN_API +Vst3PluginProxyImpl::getParamNormalized(Steinberg::Vst::ParamID id) { + return bridge.send_message(YaEditController::GetParamNormalized{ + .instance_id = instance_id(), .id = id}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::setParamNormalized(Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue value) { + return bridge.send_message(YaEditController::SetParamNormalized{ + .instance_id = instance_id(), .id = id, .value = value}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::setComponentHandler( + Steinberg::Vst::IComponentHandler* handler) { + if (handler) { + // We'll store the pointer for when the plugin later makes a callback to + // this component handler + component_handler = handler; + + // Automatically converted smart pointers for when the plugin performs a + // callback later + unit_handler = component_handler; + + return bridge.send_message(YaEditController::SetComponentHandler{ + .instance_id = instance_id(), + .component_handler_proxy_args = + Vst3ComponentHandlerProxy::ConstructArgs(component_handler, + instance_id())}); + } else { + bridge.logger.log( + "WARNING: Null pointer passed to " + "'IEditController::setComponentHandler()'"); + return Steinberg::kInvalidArgument; + } +} + +Steinberg::IPlugView* PLUGIN_API +Vst3PluginProxyImpl::createView(Steinberg::FIDString name) { + CreateViewResponse response = + bridge.send_message(YaEditController::CreateView{ + .instance_id = instance_id(), .name = name}); + + if (response.plug_view_args) { + // The host should manage this. Returning raw pointers feels scary. + auto plug_view_proxy = new Vst3PlugViewProxyImpl( + bridge, std::move(*response.plug_view_args)); + + // We also need to store an (unmanaged, since we don't want to affect + // the reference counting) pointer to this to be able to handle calls to + // `IPlugFrame::resizeView()` in the future + last_created_plug_view = plug_view_proxy; + + return plug_view_proxy; + } else { + return nullptr; + } +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::setKnobMode(Steinberg::Vst::KnobMode mode) { + return bridge.send_message(YaEditController2::SetKnobMode{ + .instance_id = instance_id(), .mode = mode}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::openHelp(TBool onlyCheck) { + return bridge.send_message(YaEditController2::OpenHelp{ + .instance_id = instance_id(), .only_check = onlyCheck}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::openAboutBox(TBool onlyCheck) { + return bridge.send_message(YaEditController2::OpenAboutBox{ + .instance_id = instance_id(), .only_check = onlyCheck}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::initialize(FUnknown* context) { + if (context) { + // We will create a proxy object that that supports all the same + // interfaces as `context`, and then we'll store `context` in this + // object. We can then use it to handle callbacks made by the Windows + // VST3 plugin to this context. + host_context = context; + + // Automatically converted smart pointers for when the plugin performs a + // callback later + host_application = host_context; + + return bridge.send_message(YaPluginBase::Initialize{ + .instance_id = instance_id(), + .host_context_args = Vst3HostContextProxy::ConstructArgs( + host_context, instance_id())}); + } else { + bridge.logger.log( + "WARNING: Null pointer passed to 'IPluginBase::initialize()'"); + return Steinberg::kInvalidArgument; + } +} + +tresult PLUGIN_API Vst3PluginProxyImpl::terminate() { + return bridge.send_message( + YaPluginBase::Terminate{.instance_id = instance_id()}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::programDataSupported( + Steinberg::Vst::ProgramListID listId) { + return bridge.send_message(YaProgramListData::ProgramDataSupported{ + .instance_id = instance_id(), .list_id = listId}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::getProgramData(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::IBStream* data) { + const GetProgramDataResponse response = bridge.send_message( + YaProgramListData::GetProgramData{.instance_id = instance_id(), + .list_id = listId, + .program_index = programIndex}); + + assert(response.data.write_back(data) == Steinberg::kResultOk); + + return response.result; +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::setProgramData(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::IBStream* data) { + return bridge.send_message( + YaProgramListData::SetProgramData{.instance_id = instance_id(), + .list_id = listId, + .program_index = programIndex, + .data = data}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::unitDataSupported(Steinberg::Vst::UnitID unitId) { + return bridge.send_message(YaUnitData::UnitDataSupported{ + .instance_id = instance_id(), .unit_id = unitId}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::getUnitData(Steinberg::Vst::UnitID unitId, + Steinberg::IBStream* data) { + const GetUnitDataResponse response = + bridge.send_message(YaUnitData::GetUnitData{ + .instance_id = instance_id(), .unit_id = unitId}); + + assert(response.data.write_back(data) == Steinberg::kResultOk); + + return response.result; +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::setUnitData(Steinberg::Vst::UnitID unitId, + Steinberg::IBStream* data) { + return bridge.send_message(YaUnitData::SetUnitData{ + .instance_id = instance_id(), .unit_id = unitId, .data = data}); +} + +int32 PLUGIN_API Vst3PluginProxyImpl::getUnitCount() { + return bridge.send_message( + YaUnitInfo::GetUnitCount{.instance_id = instance_id()}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::getUnitInfo(int32 unitIndex, + Steinberg::Vst::UnitInfo& info /*out*/) { + const GetUnitInfoResponse response = + bridge.send_message(YaUnitInfo::GetUnitInfo{ + .instance_id = instance_id(), .unit_index = unitIndex}); + + info = response.info; + + return response.result; +} + +int32 PLUGIN_API Vst3PluginProxyImpl::getProgramListCount() { + return bridge.send_message( + YaUnitInfo::GetProgramListCount{.instance_id = instance_id()}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::getProgramListInfo( + int32 listIndex, + Steinberg::Vst::ProgramListInfo& info /*out*/) { + const GetProgramListInfoResponse response = + bridge.send_message(YaUnitInfo::GetProgramListInfo{ + .instance_id = instance_id(), .list_index = listIndex}); + + info = response.info; + + return response.result; +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::getProgramName(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::Vst::String128 name /*out*/) { + const GetProgramNameResponse response = bridge.send_message( + YaUnitInfo::GetProgramName{.instance_id = instance_id(), + .list_id = listId, + .program_index = programIndex}); + + std::copy(response.name.begin(), response.name.end(), name); + name[response.name.size()] = 0; + + return response.result; +} + +tresult PLUGIN_API Vst3PluginProxyImpl::getProgramInfo( + Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::Vst::CString attributeId /*in*/, + Steinberg::Vst::String128 attributeValue /*out*/) { + assert(attributeId); + + const GetProgramInfoResponse response = bridge.send_message( + YaUnitInfo::GetProgramInfo{.instance_id = instance_id(), + .list_id = listId, + .program_index = programIndex, + .attribute_id = attributeId}); + + std::copy(response.attribute_value.begin(), response.attribute_value.end(), + attributeValue); + attributeValue[response.attribute_value.size()] = 0; + + return response.result; +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::hasProgramPitchNames(Steinberg::Vst::ProgramListID listId, + int32 programIndex) { + return bridge.send_message( + YaUnitInfo::HasProgramPitchNames{.instance_id = instance_id(), + .list_id = listId, + .program_index = programIndex}); +} + +tresult PLUGIN_API Vst3PluginProxyImpl::getProgramPitchName( + Steinberg::Vst::ProgramListID listId, + int32 programIndex, + int16 midiPitch, + Steinberg::Vst::String128 name /*out*/) { + const GetProgramPitchNameResponse response = bridge.send_message( + YaUnitInfo::GetProgramPitchName{.instance_id = instance_id(), + .list_id = listId, + .program_index = programIndex, + .midi_pitch = midiPitch}); + + std::copy(response.name.begin(), response.name.end(), name); + name[response.name.size()] = 0; + + return response.result; +} + +Steinberg::Vst::UnitID PLUGIN_API Vst3PluginProxyImpl::getSelectedUnit() { + return bridge.send_message( + YaUnitInfo::GetSelectedUnit{.instance_id = instance_id()}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::selectUnit(Steinberg::Vst::UnitID unitId) { + return bridge.send_message(YaUnitInfo::SelectUnit{ + .instance_id = instance_id(), .unit_id = unitId}); +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::getUnitByBus(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir, + int32 busIndex, + int32 channel, + Steinberg::Vst::UnitID& unitId /*out*/) { + const GetUnitByBusResponse response = bridge.send_message( + YaUnitInfo::GetUnitByBus{.instance_id = instance_id(), + .type = type, + .dir = dir, + .bus_index = busIndex, + .channel = channel}); + + unitId = response.unit_id; + + return response.result; +} + +tresult PLUGIN_API +Vst3PluginProxyImpl::setUnitProgramData(int32 listOrUnitId, + int32 programIndex, + Steinberg::IBStream* data) { + return bridge.send_message( + YaUnitInfo::SetUnitProgramData{.instance_id = instance_id(), + .list_or_unit_id = listOrUnitId, + .program_index = programIndex, + .data = data}); +} diff --git a/src/plugin/bridges/vst3-impls/plugin-proxy.h b/src/plugin/bridges/vst3-impls/plugin-proxy.h new file mode 100644 index 00000000..4a04db6c --- /dev/null +++ b/src/plugin/bridges/vst3-impls/plugin-proxy.h @@ -0,0 +1,232 @@ +// 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 . + +#pragma once + +#include "../vst3.h" +#include "plug-view-proxy.h" + +class Vst3PluginProxyImpl : public Vst3PluginProxy { + public: + Vst3PluginProxyImpl(Vst3PluginBridge& bridge, + Vst3PluginProxy::ConstructArgs&& args); + + /** + * When the reference count reaches zero and this destructor is called, + * we'll send a request to the Wine plugin host to destroy the corresponding + * object. + */ + ~Vst3PluginProxyImpl(); + + /** + * We'll override the query interface to log queries for interfaces we do + * not (yet) support. + */ + tresult PLUGIN_API queryInterface(const Steinberg::TUID _iid, + void** obj) override; + + // From `IAudioProcessor` + tresult PLUGIN_API + setBusArrangements(Steinberg::Vst::SpeakerArrangement* inputs, + int32 numIns, + Steinberg::Vst::SpeakerArrangement* outputs, + int32 numOuts) override; + tresult PLUGIN_API + getBusArrangement(Steinberg::Vst::BusDirection dir, + int32 index, + Steinberg::Vst::SpeakerArrangement& arr) override; + tresult PLUGIN_API canProcessSampleSize(int32 symbolicSampleSize) override; + uint32 PLUGIN_API getLatencySamples() override; + tresult PLUGIN_API + setupProcessing(Steinberg::Vst::ProcessSetup& setup) override; + tresult PLUGIN_API setProcessing(TBool state) override; + tresult PLUGIN_API process(Steinberg::Vst::ProcessData& data) override; + uint32 PLUGIN_API getTailSamples() override; + + // From `IComponent` + tresult PLUGIN_API getControllerClassId(Steinberg::TUID classId) override; + tresult PLUGIN_API setIoMode(Steinberg::Vst::IoMode mode) override; + int32 PLUGIN_API getBusCount(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir) override; + tresult PLUGIN_API + getBusInfo(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir, + int32 index, + Steinberg::Vst::BusInfo& bus /*out*/) override; + tresult PLUGIN_API + getRoutingInfo(Steinberg::Vst::RoutingInfo& inInfo, + Steinberg::Vst::RoutingInfo& outInfo /*out*/) override; + tresult PLUGIN_API activateBus(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir, + int32 index, + TBool state) override; + tresult PLUGIN_API setActive(TBool state) override; + tresult PLUGIN_API setState(Steinberg::IBStream* state) override; + tresult PLUGIN_API getState(Steinberg::IBStream* state) override; + + // From `IConnectionPoint` + tresult PLUGIN_API connect(IConnectionPoint* other) override; + tresult PLUGIN_API disconnect(IConnectionPoint* other) override; + tresult PLUGIN_API notify(Steinberg::Vst::IMessage* message) override; + + // From `IEditController` + tresult PLUGIN_API setComponentState(Steinberg::IBStream* state) override; + // `IEditController` also contains `getState()` and `setState()` functions. + // These are identical to those defiend in `IComponent` and they're thus + // handled in in the same function. + int32 PLUGIN_API getParameterCount() override; + tresult PLUGIN_API + getParameterInfo(int32 paramIndex, + Steinberg::Vst::ParameterInfo& info /*out*/) override; + tresult PLUGIN_API + getParamStringByValue(Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue valueNormalized /*in*/, + Steinberg::Vst::String128 string /*out*/) override; + tresult PLUGIN_API getParamValueByString( + Steinberg::Vst::ParamID id, + Steinberg::Vst::TChar* string /*in*/, + Steinberg::Vst::ParamValue& valueNormalized /*out*/) override; + Steinberg::Vst::ParamValue PLUGIN_API + normalizedParamToPlain(Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue valueNormalized) override; + Steinberg::Vst::ParamValue PLUGIN_API + plainParamToNormalized(Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue plainValue) override; + Steinberg::Vst::ParamValue PLUGIN_API + getParamNormalized(Steinberg::Vst::ParamID id) override; + tresult PLUGIN_API + setParamNormalized(Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue value) override; + tresult PLUGIN_API + setComponentHandler(Steinberg::Vst::IComponentHandler* handler) override; + Steinberg::IPlugView* PLUGIN_API + createView(Steinberg::FIDString name) override; + + // From `IEditController2` + tresult PLUGIN_API setKnobMode(Steinberg::Vst::KnobMode mode) override; + tresult PLUGIN_API openHelp(TBool onlyCheck) override; + tresult PLUGIN_API openAboutBox(TBool onlyCheck) override; + + // From `IPluginBase` + tresult PLUGIN_API initialize(FUnknown* context) override; + tresult PLUGIN_API terminate() override; + + // From `IProgramListData` + tresult PLUGIN_API + programDataSupported(Steinberg::Vst::ProgramListID listId) override; + tresult PLUGIN_API getProgramData(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::IBStream* data) override; + tresult PLUGIN_API setProgramData(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::IBStream* data) override; + + // From `IUnitData` + tresult PLUGIN_API + unitDataSupported(Steinberg::Vst::UnitID unitId) override; + tresult PLUGIN_API getUnitData(Steinberg::Vst::UnitID unitId, + Steinberg::IBStream* data) override; + tresult PLUGIN_API setUnitData(Steinberg::Vst::UnitID unitId, + Steinberg::IBStream* data) override; + + // From `IUnitInfo` + int32 PLUGIN_API getUnitCount() override; + tresult PLUGIN_API + getUnitInfo(int32 unitIndex, + Steinberg::Vst::UnitInfo& info /*out*/) override; + int32 PLUGIN_API getProgramListCount() override; + tresult PLUGIN_API + getProgramListInfo(int32 listIndex, + Steinberg::Vst::ProgramListInfo& info /*out*/) override; + tresult PLUGIN_API + getProgramName(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::Vst::String128 name /*out*/) override; + tresult PLUGIN_API + getProgramInfo(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + Steinberg::Vst::CString attributeId /*in*/, + Steinberg::Vst::String128 attributeValue /*out*/) override; + tresult PLUGIN_API + hasProgramPitchNames(Steinberg::Vst::ProgramListID listId, + int32 programIndex) override; + tresult PLUGIN_API + getProgramPitchName(Steinberg::Vst::ProgramListID listId, + int32 programIndex, + int16 midiPitch, + Steinberg::Vst::String128 name /*out*/) override; + Steinberg::Vst::UnitID PLUGIN_API getSelectedUnit() override; + tresult PLUGIN_API selectUnit(Steinberg::Vst::UnitID unitId) override; + tresult PLUGIN_API + getUnitByBus(Steinberg::Vst::MediaType type, + Steinberg::Vst::BusDirection dir, + int32 busIndex, + int32 channel, + Steinberg::Vst::UnitID& unitId /*out*/) override; + tresult PLUGIN_API setUnitProgramData(int32 listOrUnitId, + int32 programIndex, + Steinberg::IBStream* data) override; + + /** + * The component handler the host passed to us during + * `IEditController::setComponentHandler()`. When the plugin makes a + * callback on a component handler proxy object, we'll pass the call through + * to this object. + */ + Steinberg::IPtr component_handler; + + /** + * If the host doesn't connect two objects directly in + * `IConnectionPoint::connect` but instead connects them through a proxy, + * we'll store that proxy here. This way we can then route messages sent by + * the plugin through this proxy. So far this is only needed for Ardour. + */ + Steinberg::IPtr connection_point_proxy; + + /** + * An unmanaged, raw pointer to the `IPlugView` instance returned in our + * implementation of `IEditController::createView()`. We need this to handle + * `IPlugFrame::resizeView()`, since that expects a pointer to the view that + * gets resized. + * + * XXX: This approach of course won't work with multiple views, but the SDK + * currently only defines a single type of view so that shouldn't be an + * issue + */ + Vst3PlugViewProxyImpl* last_created_plug_view = nullptr; + + // The following pointers are cast from `host_context` if + // `IPluginBase::initialize()` has been called + + Steinberg::FUnknownPtr host_application; + + // The following pointers are cast from `component_handler` if + // `IEditController::setComponentHandler()` has been called + + Steinberg::FUnknownPtr unit_handler; + + private: + Vst3PluginBridge& bridge; + + /** + * An host context if we get passed one through `IPluginBase::initialize()`. + * We'll read which interfaces it supports and we'll then create a proxy + * object that supports those same interfaces. This should be the same for + * all plugin instances so we should not have to store it here separately, + * but for the sake of correctness we will. + */ + Steinberg::IPtr host_context; +}; diff --git a/src/plugin/bridges/vst3.cpp b/src/plugin/bridges/vst3.cpp new file mode 100644 index 00000000..b59564ef --- /dev/null +++ b/src/plugin/bridges/vst3.cpp @@ -0,0 +1,220 @@ +// 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 . + +#include "vst3.h" + +#include "src/common/serialization/vst3.h" +#include "vst3-impls/plugin-factory.h" +#include "vst3-impls/plugin-proxy.h" + +// There are still some design decisions that need some more thought +// TODO: The documentation mentions that private communication through VST3's +// message system should be handled on a separate timer thread. Do we +// need special handling for this on the Wine side (e.g. during the event +// handling loop)? Probably not, since the actual host should manage all +// messaging. +// TODO: The docs very explicitly mention that +// the`IComponentHandler::{begin,perform,end}Edit()` functions have to be +// called from the UI thread. Should we have special handling for this or +// does everything just magically work out? +// TODO: Something that's not relevant here but that will require some thinking +// is that VST3 requires all plugins to be installed in ~/.vst3. I can +// think of two options and I"m not sure what's the best one: +// +// 1. We can add the required files for the Linux VST3 plugin to the +// location of the Windows VST3 plugin (by adding some files to the +// bundle or creating a bundle next to it) and then symlink that bundle +// to ~/.vst3. +// 2. We can create the bundle in ~/.vst3 and symlink the Windows plugin +// and all of its resources into bundle as if they were also installed +// there. +// +// The second one sounds much better, but it will still need some more +// consideration. Aside from that VST3 plugins also have a centralized +// preset location, even though barely anyone uses it, yabridgectl will +// also have to make a symlink of that. Also, yabridgectl will need to do +// some extra work there to detect removed plugins. +// TODO: Also symlink presets, and allow pruning broken symlinks there as well +// TODO: And how do we choose between 32-bit and 64-bit versions of a VST3 +// plugin if they exist? Config files? + +Vst3PluginBridge::Vst3PluginBridge() + : PluginBridge( + PluginType::vst3, + [](boost::asio::io_context& io_context, const PluginInfo& info) { + return Vst3Sockets( + io_context, + generate_endpoint_base(info.native_library_path.filename() + .replace_extension("") + .string()), + true); + }), + logger(generic_logger) { + log_init_message(); + + // This will block until all sockets have been connected to by the Wine VST + // host + connect_sockets_guarded(); + + // Now that communication is set up the Wine host can send callbacks to this + // bridge class, and we can send control messages to the Wine host. This + // messaging mechanism is how we relay the VST3 communication protocol. As a + // first thing, the Wine VST host will ask us for a copy of the + // configuration. + host_callback_handler = std::jthread([&]() { + sockets.vst_host_callback.receive_messages( + std::pair(logger, false), + overload{ + [&](const WantsConfiguration&) -> WantsConfiguration::Response { + return config; + }, + [&](const YaComponentHandler::BeginEdit& request) + -> YaComponentHandler::BeginEdit::Response { + return plugin_proxies.at(request.owner_instance_id) + .get() + .component_handler->beginEdit(request.id); + }, + [&](const YaComponentHandler::PerformEdit& request) + -> YaComponentHandler::PerformEdit::Response { + return plugin_proxies.at(request.owner_instance_id) + .get() + .component_handler->performEdit( + request.id, request.value_normalized); + }, + [&](const YaComponentHandler::EndEdit& request) + -> YaComponentHandler::EndEdit::Response { + return plugin_proxies.at(request.owner_instance_id) + .get() + .component_handler->endEdit(request.id); + }, + [&](const YaComponentHandler::RestartComponent& request) + -> YaComponentHandler::EndEdit::Response { + return plugin_proxies.at(request.owner_instance_id) + .get() + .component_handler->restartComponent(request.flags); + }, + [&](YaConnectionPoint::Notify& request) + -> YaConnectionPoint::Notify::Response { + return plugin_proxies.at(request.instance_id) + .get() + .connection_point_proxy->notify(&request.message_ptr); + }, + [&](const YaHostApplication::GetName& request) + -> YaHostApplication::GetName::Response { + tresult result; + Steinberg::Vst::String128 name{0}; + if (request.owner_instance_id) { + result = plugin_proxies.at(*request.owner_instance_id) + .get() + .host_application->getName(name); + } else { + result = + plugin_factory->host_application->getName(name); + } + + return YaHostApplication::GetNameResponse{ + .result = result, + .name = tchar_pointer_to_u16string(name), + }; + }, + [&](YaPlugFrame::ResizeView& request) + -> YaPlugFrame::ResizeView::Response { + // XXX: As mentioned elsewhere, since VST3 only supports a + // single plug view type at the moment we'll just + // assume that this function is called from the last + // (and only) `IPlugView*` instance returned by the + // plugin. + Vst3PlugViewProxyImpl* plug_view = + plugin_proxies.at(request.owner_instance_id) + .get() + .last_created_plug_view; + + return plug_view->plug_frame->resizeView(plug_view, + &request.new_size); + }, + [&](const YaUnitHandler::NotifyUnitSelection& request) + -> YaUnitHandler::NotifyUnitSelection::Response { + return plugin_proxies.at(request.owner_instance_id) + .get() + .unit_handler->notifyUnitSelection(request.unit_id); + }, + [&](const YaUnitHandler::NotifyProgramListChange& request) + -> YaUnitHandler::NotifyProgramListChange::Response { + return plugin_proxies.at(request.owner_instance_id) + .get() + .unit_handler->notifyProgramListChange( + request.list_id, request.program_index); + }, + }); + }); +} + +Vst3PluginBridge::~Vst3PluginBridge() { + // Drop all work make sure all sockets are closed + plugin_host->terminate(); + io_context.stop(); +} + +Steinberg::IPluginFactory* Vst3PluginBridge::get_plugin_factory() { + // Even though we're working with raw pointers here, we should pretend that + // we're `IPtr` and do the reference counting + // ourselves. This should work the same was as the standard implementation + // in `public.sdk/source/main/pluginfactory.h`. If we were to use an IPtr or + // an STL smart pointer we would get a double free (or rather, a use after + // free). + if (plugin_factory) { + plugin_factory->addRef(); + } else { + // Set up the plugin factory, since this is the first thing the host + // will request after loading the module. Host callback handlers should + // have started before this since the Wine plugin host will request a + // copy of the configuration during its initialization. + YaPluginFactory::ConstructArgs factory_args = + sockets.host_vst_control.send_message( + YaPluginFactory::Construct{}, + std::pair(logger, true)); + plugin_factory = + new YaPluginFactoryImpl(*this, std::move(factory_args)); + } + + return plugin_factory; +} + +void Vst3PluginBridge::register_plugin_proxy( + Vst3PluginProxyImpl& proxy_object) { + std::lock_guard lock(plugin_proxies_mutex); + + plugin_proxies.emplace(proxy_object.instance_id(), + std::ref(proxy_object)); + + // For optimization reaons we use dedicated sockets for functions that will + // be run in the audio processing loop + if (proxy_object.YaAudioProcessor::supported() || + proxy_object.YaComponent::supported()) { + sockets.add_audio_processor_and_connect(proxy_object.instance_id()); + } +} + +void Vst3PluginBridge::unregister_plugin_proxy( + Vst3PluginProxyImpl& proxy_object) { + std::lock_guard lock(plugin_proxies_mutex); + + plugin_proxies.erase(proxy_object.instance_id()); + if (proxy_object.YaAudioProcessor::supported() || + proxy_object.YaComponent::supported()) { + sockets.remove_audio_processor(proxy_object.instance_id()); + } +} diff --git a/src/plugin/bridges/vst3.h b/src/plugin/bridges/vst3.h new file mode 100644 index 00000000..21deb51b --- /dev/null +++ b/src/plugin/bridges/vst3.h @@ -0,0 +1,163 @@ +// 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 . + +#pragma once + +#include + +#include "../..//common/serialization/vst3/plugin-factory.h" +#include "../../common/communication/vst3.h" +#include "../../common/logging/vst3.h" +#include "common.h" + +// Forward declarations +class Vst3PluginProxyImpl; +class YaPluginFactoryImpl; + +/** + * This handles the communication between the native host and a VST3 plugin + * hosted in our Wine plugin host. VST3 is handled very differently from VST2 + * because a plugin is no longer its own entity, but rather a definition of + * objects that the host can create and interconnect. This `Vst3PluginBridge` + * will be instantiated when the plugin first gets loaded, and it will survive + * until the last instance of the plugin gets removed. The Wine host process + * will thus also have the same lifetime, and even with yabridge's 'individual' + * plugin hosting other instances of the same plugin will be handled by a single + * process. + * + * @remark See the comments at the top of `vst3-plugin.cpp` for more + * information. + * + * The naming scheme of all of these 'bridge' classes is `{,Plugin}Bridge` + * for greppability reasons. The `Plugin` infix is added on the native plugin + * side. + */ +class Vst3PluginBridge : PluginBridge> { + public: + /** + * Initializes the VST3 module by starting and setting up communicating with + * the Wine plugin host. + * + * @throw std::runtime_error Thrown when the Wine plugin host could not be + * found, or if it could not locate and load a VST3 module. + */ + Vst3PluginBridge(); + + /** + * Terminate the Wine plugin host process and drop all work when the module + * gets unloaded. + */ + ~Vst3PluginBridge(); + + /** + * When the host loads the module it will call `GetPluginFactory()` which + * will in turn call this function. The idea is that we return an + * `IPluginFactory*` while doing all the reference counting that `IPtr` + * would normally do for us ourselves. This means that when the host frees + * its last instance of this factory, `plugin_factory` will also be cleared. + * + * @see plugin_factory + */ + Steinberg::IPluginFactory* get_plugin_factory(); + + /** + * Add a `Vst3PluginProxyImpl` to the list of registered proxy objects so we + * can handle host callbacks. This function is called in + * `Vst3PluginProxyImpl`'s constructor. If the plugin supports the + * `IAudioProcessor` or `IComponent` interfaces, then we'll also connect to + * a dedicated audio processing socket. + * + * @param proxy_object The proxy object so we can access its host context + * and unique instance identifier. + * + * @see plugin_proxies + */ + void register_plugin_proxy(Vst3PluginProxyImpl& proxy_object); + + /** + * Remove a previously registered `Vst3PluginProxyImpl` from the list of + * registered proxy objects. Called during the object's destructor after + * asking the Wine plugin host to destroy the component on its side. + * + * @param proxy_object The proxy object so we can access its unique instance + * identifier. + * + * @see plugin_proxies + */ + void unregister_plugin_proxy(Vst3PluginProxyImpl& proxy_object); + + /** + * Send a control message to the Wine plugin host return the response. This + * is a shorthand for `sockets.host_vst_control.send_message` for use in + * VST3 interface implementations. + */ + template + typename T::Response send_message(const T& object) { + return sockets.host_vst_control.send_message( + object, std::pair(logger, true)); + } + + /** + * Send an `IAudioProcessor` or `IComponent` control message to a specific + * plugin instance. This is separated from the above `send_message()` for + * performance reasons, as this way every instance has its own dedicated + * socket and thread. + */ + template + typename T::Response send_audio_processor_message(const T& object) { + return sockets.send_audio_processor_message( + object, std::pair(logger, true)); + } + + /** + * The logging facility used for this instance of yabridge. Wraps around + * `PluginBridge::generic_logger`. + */ + Vst3Logger logger; + + private: + /** + * Handles callbacks from the plugin to the host over the + * `vst_host_callback` sockets. + */ + std::jthread host_callback_handler; + + /** + * Our plugin factory. All information about the plugin and its supported + * classes are copied directly from the Windows VST3 plugin's factory on the + * Wine side, and we'll provide an implementation that can send control + * messages to the Wine plugin host. As explained in `get_plugin_factory()`, + * this cannot be a smart pointer because the factory is supposed to free + * itself when the host removes its last adopted `IPtr`. + * + * @related get_plugin_factory + */ + YaPluginFactoryImpl* plugin_factory = nullptr; + + private: + /** + * All VST3 plugin objects we created from this plugin. We keep track of + * these in case the plugin does a host callback, so we can associate that + * call with the exact host context object passed to it during a call to + * `initialize()`. The IDs here are the same IDs as generated by the Wine + * plugin host. An instance is added here through a call by + * `register_plugin_proxy()` in the constractor, and an instance is then + * removed through a call to `unregister_plugin_proxy()` in the destructor. + */ + std::map> + plugin_proxies; + std::mutex plugin_proxies_mutex; +}; diff --git a/src/plugin/host-process.cpp b/src/plugin/host-process.cpp index 930209f8..2ed9a9bf 100644 --- a/src/plugin/host-process.cpp +++ b/src/plugin/host-process.cpp @@ -21,8 +21,6 @@ #include #include -#include "../common/communication.h" - namespace bp = boost::process; namespace fs = boost::filesystem; @@ -88,38 +86,39 @@ void HostProcess::async_log_pipe_lines(patched_async_pipe& pipe, IndividualHost::IndividualHost(boost::asio::io_context& io_context, Logger& logger, - fs::path plugin_path, - const Sockets& sockets) + const PluginInfo& plugin_info, + const HostRequest& host_request) : HostProcess(io_context, logger), - plugin_arch(find_vst_architecture(plugin_path)), - host_path(find_vst_host(plugin_arch, false)), - host(launch_host(host_path, + plugin_info(plugin_info), + host_path(find_vst_host(plugin_info.native_library_path, + plugin_info.plugin_arch, + false)), + host(launch_host( + host_path, + plugin_type_to_string(host_request.plugin_type), #ifdef WITH_WINEDBG - plugin_path.filename(), + plugin_info.windows_plugin_path.filename(), #else - plugin_path, + host_request.plugin_path, #endif - sockets.base_dir, - bp::env = set_wineprefix(), - bp::std_out = stdout_pipe, - bp::std_err = stderr_pipe + host_request.endpoint_base_dir, + bp::env = plugin_info.create_host_env(), + bp::std_out = stdout_pipe, + bp::std_err = stderr_pipe #ifdef WITH_WINEDBG - , // winedbg has no reliable way to escape spaces, so - // we'll start the process in the plugin's directory - bp::start_dir = plugin_path.parent_path() + , // winedbg has no reliable way to escape spaces, so + // we'll start the process in the plugin's directory + bp::start_dir = plugin_info.windows_plugin_path.parent_path() #endif - )) { + )) { #ifdef WITH_WINEDBG - if (plugin_path.filename().string().find(' ') != std::string::npos) { + if (plugin_info.windows_plugin_path.filename().string().find(' ') != + std::string::npos) { logger.log("Warning: winedbg does not support paths containing spaces"); } #endif } -PluginArchitecture IndividualHost::architecture() { - return plugin_arch; -} - fs::path IndividualHost::path() { return host_path; } @@ -135,15 +134,19 @@ void IndividualHost::terminate() { GroupHost::GroupHost(boost::asio::io_context& io_context, Logger& logger, - fs::path plugin_path, - Sockets& sockets, + const PluginInfo& plugin_info, + const HostRequest& host_request, + Sockets& sockets, std::string group_name) : HostProcess(io_context, logger), - plugin_arch(find_vst_architecture(plugin_path)), - host_path(find_vst_host(plugin_arch, true)), + plugin_info(plugin_info), + host_path(find_vst_host(plugin_info.native_library_path, + plugin_info.plugin_arch, + true)), sockets(sockets) { #ifdef WITH_WINEDBG - if (plugin_path.string().find(' ') != std::string::npos) { + if (plugin_info.windows_plugin_path.string().find(' ') != + std::string::npos) { logger.log("Warning: winedbg does not support paths containing spaces"); } #endif @@ -156,35 +159,17 @@ GroupHost::GroupHost(boost::asio::io_context& io_context, // other processes will exit. When a plugin's host process has exited, it // will try to connect to the socket once more in the case that another // process is now listening on it. - const bp::environment host_env = set_wineprefix(); - fs::path wine_prefix; - if (auto wine_prefix_envvar = host_env.find("WINEPREFIX"); - wine_prefix_envvar != host_env.end()) { - // This is a bit ugly, but Boost.Process's environment does not have a - // graceful way to check for empty environment variables in const - // qualified environments - wine_prefix = wine_prefix_envvar->to_string(); - } else { - // 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 endpoint_base_dir = sockets.base_dir; const fs::path group_socket_path = - generate_group_endpoint(group_name, wine_prefix, plugin_arch); - const auto connect = [&io_context, plugin_path, endpoint_base_dir, + generate_group_endpoint(group_name, plugin_info.normalize_wine_prefix(), + plugin_info.plugin_arch); + const auto connect = [&io_context, host_request, endpoint_base_dir, group_socket_path]() { boost::asio::local::stream_protocol::socket group_socket(io_context); group_socket.connect(group_socket_path.string()); - write_object( - group_socket, - GroupRequest{.plugin_path = plugin_path.string(), - .endpoint_base_dir = endpoint_base_dir.string()}); - const auto response = read_object(group_socket); + write_object(group_socket, host_request); + const auto response = read_object(group_socket); assert(response.pid > 0); }; @@ -197,14 +182,14 @@ GroupHost::GroupHost(boost::asio::io_context& io_context, // because it should run independently of this yabridge instance as // it will likely outlive it. bp::child group_host = - launch_host(host_path, group_socket_path, bp::env = host_env, + launch_host(host_path, group_socket_path, + bp::env = plugin_info.create_host_env(), bp::std_out = stdout_pipe, bp::std_err = stderr_pipe); group_host.detach(); const pid_t group_host_pid = group_host.id(); group_host_connect_handler = - std::jthread([this, connect, group_socket_path, plugin_path, - endpoint_base_dir, group_host_pid]() { + std::jthread([this, connect, group_host_pid]() { using namespace std::literals::chrono_literals; // We'll first try to connect to the group host we just spawned @@ -235,10 +220,6 @@ GroupHost::GroupHost(boost::asio::io_context& io_context, } } -PluginArchitecture GroupHost::architecture() { - return plugin_arch; -} - fs::path GroupHost::path() { return host_path; } @@ -253,8 +234,8 @@ bool GroupHost::running() { void GroupHost::terminate() { // There's no need to manually terminate group host processes as they will // shut down automatically after all plugins have exited. Manually closing - // the dispatch socket will cause the associated plugin to exit. - sockets.host_vst_dispatch.close(); + // the sockets will cause the associated plugin to exit. + sockets.close(); } bool pid_running(pid_t pid) { diff --git a/src/plugin/host-process.h b/src/plugin/host-process.h index c21ecf16..cc950c81 100644 --- a/src/plugin/host-process.h +++ b/src/plugin/host-process.h @@ -16,6 +16,8 @@ #pragma once +#include + // Boost.Process's auto detection for vfork() support doesn't seem to work #define BOOST_POSIX_HAS_VFORK 1 @@ -23,10 +25,11 @@ #include #include #include -#include -#include "../common/communication.h" -#include "../common/logging.h" +#include "../common/communication/common.h" +#include "../common/logging/common.h" +#include "../common/plugins.h" +#include "../common/serialization/common.h" #include "utils.h" /** @@ -39,12 +42,6 @@ class HostProcess { public: virtual ~HostProcess(){}; - /** - * Return the architecture of the plugin we are loading, i.e. whether it is - * 32-bit or 64-bit. - */ - virtual PluginArchitecture architecture() = 0; - /** * Return the full path to the host application in use. The host application * is chosen depending on the architecture of the plugin's DLL file and on @@ -118,24 +115,26 @@ class IndividualHost : public HostProcess { * handled on. * @param logger The `Logger` instance the redirected STDIO streams will be * written to. - * @param sockets The socket endpoints that will be used for communication - * with the plugin. + * @param plugin_info Information about the plugin we're going to use. Used + * to retrieve the Wine prefix and the plugin's architecture. + * @param host_request The information about the plugin we should launch a + * host process for. The values in the struct will be used as command line + * arguments. * * @throw std::runtime_error When `plugin_path` does not point to a valid * 32-bit or 64-bit .dll file. */ IndividualHost(boost::asio::io_context& io_context, Logger& logger, - boost::filesystem::path plugin_path, - const Sockets& sockets); + const PluginInfo& plugin_info, + const HostRequest& host_request); - PluginArchitecture architecture() override; boost::filesystem::path path() override; bool running() override; void terminate() override; private: - PluginArchitecture plugin_arch; + const PluginInfo& plugin_info; boost::filesystem::path host_path; boost::process::child host; }; @@ -161,6 +160,10 @@ class GroupHost : public HostProcess { * handled on. * @param logger The `Logger` instance the redirected STDIO streams will be * written to. + * @param plugin_info Information about the plugin we're going to use. Used + * to retrieve the Wine prefix and the plugin's architecture. + * @param host_request The information about the plugin we should launch a + * host process for. This object will be sent to the group host process. * @param sockets The socket endpoints that will be used for communication * with the plugin. When the plugin shuts down, we'll terminate the * dispatch socket contained in this object. @@ -168,17 +171,17 @@ class GroupHost : public HostProcess { */ GroupHost(boost::asio::io_context& io_context, Logger& logger, - boost::filesystem::path plugin_path, - Sockets& socket_endpoint, + const PluginInfo& plugin_info, + const HostRequest& host_request, + Sockets& socket_endpoint, std::string group_name); - PluginArchitecture architecture() override; boost::filesystem::path path() override; bool running() override; void terminate() override; private: - PluginArchitecture plugin_arch; + const PluginInfo& plugin_info; boost::filesystem::path host_path; /** @@ -203,7 +206,7 @@ class GroupHost : public HostProcess { * The associated sockets for the plugin we're hosting. This is used to * terminate the plugin. */ - Sockets& sockets; + Sockets& sockets; /** * A thread that waits for the group host to have started and then ask it to diff --git a/src/plugin/utils.cpp b/src/plugin/utils.cpp index 01b91d9a..24dc4b1a 100644 --- a/src/plugin/utils.cpp +++ b/src/plugin/utils.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include // Generated inside of the build directory @@ -33,6 +32,230 @@ namespace bp = boost::process; namespace fs = boost::filesystem; +// These functions are used to populate the fields in `PluginInfo`. See the +// docstrings for the corresponding fields for more information on what we're +// actually doing here. +fs::path find_plugin_library(const fs::path& this_plugin_path, + PluginType plugin_type); +fs::path normalize_plugin_path(const fs::path& windows_library_path, + PluginType plugin_type); +std::variant find_wine_prefix( + fs::path windows_plugin_path); + +PluginInfo::PluginInfo(PluginType plugin_type) + : plugin_type(plugin_type), + native_library_path(get_this_file_location()), + // As explained in the docstring, this is the actual Windows library. For + // VST3 plugins that come in a module we should be loading that module + // instead of the `.vst3` file within in, which is where + // `windows_plugin_path` comes in. + windows_library_path( + find_plugin_library(native_library_path, plugin_type)), + plugin_arch(find_dll_architecture(windows_library_path)), + windows_plugin_path( + normalize_plugin_path(windows_library_path, plugin_type)), + wine_prefix(find_wine_prefix(windows_plugin_path)) {} + +bp::environment PluginInfo::create_host_env() const { + bp::environment env = boost::this_process::environment(); + + // Only set the prefix when could auto detect it and it's not being + // overridden (this entire `std::visit` instead of `std::has_alternative` is + // just for clarity's sake) + std::visit(overload{ + [](const OverridenWinePrefix&) {}, + [&](const boost::filesystem::path& prefix) { + env["WINEPREFIX"] = prefix.string(); + }, + [](const DefaultWinePrefix&) {}, + }, + wine_prefix); + + return env; +} + +boost::filesystem::path PluginInfo::normalize_wine_prefix() const { + return std::visit( + overload{ + [](const OverridenWinePrefix& prefix) { return prefix.value; }, + [](const boost::filesystem::path& prefix) { return prefix; }, + [](const DefaultWinePrefix&) { + const bp::environment env = boost::this_process::environment(); + return fs::path(env.at("HOME").to_string()) / ".wine"; + }, + }, + wine_prefix); +} + +fs::path find_plugin_library(const fs::path& this_plugin_path, + PluginType plugin_type) { + switch (plugin_type) { + case PluginType::vst2: { + fs::path plugin_path(this_plugin_path); + plugin_path.replace_extension(".dll"); + if (fs::exists(plugin_path)) { + // Also resolve symlinks here, to support symlinked .dll files + return fs::canonical(plugin_path); + } + + // In case this files does not exist and our `.so` file is a + // symlink, we'll also repeat this check after resolving that + // symlink to support links to copies of `libyabridge-vst2.so` as + // described in issue #3 + fs::path alternative_plugin_path = fs::canonical(this_plugin_path); + alternative_plugin_path.replace_extension(".dll"); + if (fs::exists(alternative_plugin_path)) { + return fs::canonical(alternative_plugin_path); + } + + // This function is used in the constructor's initializer list so we + // have to throw when the path could not be found + throw std::runtime_error("'" + plugin_path.string() + + "' does not exist, make sure to rename " + "'libyabridge-vst2.so' to match a " + "VST plugin .dll file."); + } break; + case PluginType::vst3: { + // A VST3 plugin in Linux always has to be inside of a bundle (= + // directory) named `X.vst3` that contains a static object + // `X.vst3/Contents/x86_64-linux/X.so`. On Linux `X.so` is not + // allowed to be standalone, so for yabridge this should also always + // be installed this way. + // https://developer.steinberg.help/pages/viewpage.action?pageId=9798275 + const fs::path bundle_home = + this_plugin_path.parent_path().parent_path().parent_path(); + const fs::path win_module_name = + this_plugin_path.filename().replace_extension(".vst3"); + + // Quick check in case the plugin was set up without yabridgectl, + // since the format is very specific and any deviations from that + // will be incorrect. + if (bundle_home.extension() != ".vst3") { + throw std::runtime_error( + "'" + this_plugin_path.string() + + "' is not inside of a VST3 bundle. Use yabridgectl to " + "set up yabridge for VST3 plugins or check the readme " + "for the correct format."); + } + + // Finding the Windows plugin consists of two steps because + // Steinberg changed the format around: + // - First we'll find the plugin in the VST3 bundle created by + // yabridgectl in `~/.vst3`. The plugin can be either 32-bit or + // 64-bit. + // TODO: Right now we can't select between the 64-bit and the + // 32-bit version and we'll just pick whichever one is + // available + // - After that we'll resolve the symlink to the module in the Wine + // prefix, and then we'll have to figure out if this module is an + // old style standalone module (< 3.6.10) or if it's inside of + // a bundle (>= 3.6.10) + fs::path candidate_path = + bundle_home / "Contents" / "x86_64-win" / win_module_name; + if (!fs::exists(candidate_path)) { + // Try the 32-bit version no 64-bit version exists (although, is + // there a single VST3 plugin where this is the case?) + fs::path candidate_path = + bundle_home / "Contents" / "x86-win" / win_module_name; + } + + // After this we'll have to use `normalize_plugin_path()` to get the + // actual module entry point in case the plugin is using a VST + // 3.6.10 style bundle + if (fs::exists(candidate_path)) { + return fs::canonical(candidate_path); + } + + throw std::runtime_error( + "'" + bundle_home.string() + + "' does not contain a Windows VST3 module. Use yabridgectl to " + "set up yabridge for VST3 plugins or check the readme " + "for the correct format."); + } break; + default: + throw std::runtime_error("How did you manage to get this?"); + break; + } +} + +fs::path normalize_plugin_path(const fs::path& windows_library_path, + PluginType plugin_type) { + switch (plugin_type) { + case PluginType::vst2: + return windows_library_path; + break; + case PluginType::vst3: { + // Now we'll have to figure out if this is a new-style bundle or + // an old standalone module + const fs::path win_module_name = + windows_library_path.filename().replace_extension(".vst3"); + const fs::path windows_bundle_home = + windows_library_path.parent_path().parent_path().parent_path(); + if (equals_case_insensitive(windows_bundle_home.filename().string(), + win_module_name.string())) { + return windows_bundle_home; + } else { + return windows_library_path; + } + } break; + default: + throw std::runtime_error("How did you manage to get this?"); + break; + } +} + +std::variant find_wine_prefix( + fs::path windows_plugin_path) { + bp::environment env = boost::this_process::environment(); + if (!env["WINEPREFIX"].empty()) { + return OverridenWinePrefix{env["WINEPREFIX"].to_string()}; + } + + std::optional dosdevices_dir = find_dominating_file( + "dosdevices", windows_plugin_path, fs::is_directory); + if (!dosdevices_dir) { + return DefaultWinePrefix{}; + } + + return dosdevices_dir->parent_path(); +} + +fs::path get_this_file_location() { + // HACK: Not sure why, but `boost::dll::this_line_location()` returns a path + // starting with a double slash on some systems. I've seen this happen + // 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. 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().starts_with("//")) { + this_file = ("/" / this_file).lexically_normal(); + } + + return this_file; +} + +bool equals_case_insensitive(const std::string& a, const std::string& b) { + return std::equal(a.begin(), a.end(), b.begin(), + [](const char& a_char, const char& b_char) { + return std::tolower(a_char) == std::tolower(b_char); + }); +} + +std::string join_quoted_strings(std::vector& strings) { + bool is_first = true; + std::ostringstream joined_strings{}; + for (const auto& option : strings) { + joined_strings << (is_first ? "'" : ", '") << option << "'"; + is_first = false; + } + + return joined_strings.str(); +} + std::string create_logger_prefix(const fs::path& endpoint_base_dir) { // Use the name of the base directory used for our sockets as the logger // prefix, but strip the `yabridge-` part since that's redundant @@ -45,73 +268,20 @@ std::string create_logger_prefix(const fs::path& endpoint_base_dir) { return "[" + endpoint_name + "] "; } -std::optional find_wineprefix() { - std::optional dosdevices_dir = - find_dominating_file("dosdevices", find_vst_plugin(), fs::is_directory); - if (!dosdevices_dir) { - return std::nullopt; - } - - return dosdevices_dir->parent_path(); -} - -PluginArchitecture find_vst_architecture(fs::path plugin_path) { - std::ifstream file(plugin_path, std::ifstream::binary | std::ifstream::in); - - // The linker will place the offset where the PE signature is placed at the - // end of the MS-DOS stub, at offset 0x3c - uint32_t pe_signature_offset; - file.seekg(0x3c); - file.read(reinterpret_cast(&pe_signature_offset), - sizeof(pe_signature_offset)); - - // The PE32 signature will be followed by a magic number that indicates the - // target architecture of the binary - uint32_t pe_signature; - uint16_t machine_type; - file.seekg(pe_signature_offset); - file.read(reinterpret_cast(&pe_signature), sizeof(pe_signature)); - file.read(reinterpret_cast(&machine_type), sizeof(machine_type)); - - constexpr char expected_pe_signature[4] = {'P', 'E', '\0', '\0'}; - if (pe_signature != - *reinterpret_cast(expected_pe_signature)) { - throw std::runtime_error("'" + plugin_path.string() + - "' is not a valid .dll file"); - } - - // These constants are specified in - // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types - switch (machine_type) { - case 0x014c: // IMAGE_FILE_MACHINE_I386 - return PluginArchitecture::vst_32; - break; - case 0x8664: // IMAGE_FILE_MACHINE_AMD64 - case 0x0000: // IMAGE_FILE_MACHINE_UNKNOWN - return PluginArchitecture::vst_64; - break; - } - - // When compiled without optimizations, GCC 9.3 will warn that the function - // does not return if we put this in a `default:` case instead. - std::ostringstream error_msg; - error_msg << "'" << plugin_path - << "' is neither a x86 nor a x86_64 PE32 file. Actual " - "architecture: 0x" - << std::hex << machine_type; - throw std::runtime_error(error_msg.str()); -} - -fs::path find_vst_host(PluginArchitecture plugin_arch, bool use_plugin_groups) { +fs::path find_vst_host(const boost::filesystem::path& this_plugin_path, + LibArchitecture 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) { + if (plugin_arch == LibArchitecture::dll_32) { host_name = use_plugin_groups ? yabridge_group_host_name_32bit : yabridge_individual_host_name_32bit; } + // If our `.so` file is a symlink, then search for the host in the directory + // of the file that symlink points to fs::path host_path = - fs::canonical(get_this_file_location()).remove_filename() / host_name; + fs::canonical(this_plugin_path).remove_filename() / host_name; if (fs::exists(host_path)) { return host_path; } @@ -128,47 +298,20 @@ fs::path find_vst_host(PluginArchitecture plugin_arch, bool use_plugin_groups) { return vst_host_path; } -fs::path find_vst_plugin() { - const fs::path this_plugin_path = get_this_file_location(); - - fs::path plugin_path(this_plugin_path); - plugin_path.replace_extension(".dll"); - if (fs::exists(plugin_path)) { - // Also resolve symlinks here, to support symlinked .dll files - return fs::canonical(plugin_path); - } - - // In case this files does not exist and our `.so` file is a symlink, we'll - // also repeat this check after resolving that symlink to support links to - // copies of `libyabridge.so` as described in issue #3 - fs::path alternative_plugin_path = fs::canonical(this_plugin_path); - alternative_plugin_path.replace_extension(".dll"); - if (fs::exists(alternative_plugin_path)) { - return fs::canonical(alternative_plugin_path); - } - - // This function is used in the constructor's initializer list so we have to - // throw when the path could not be found - throw std::runtime_error("'" + plugin_path.string() + - "' does not exist, make sure to rename " - "'libyabridge.so' to match a " - "VST plugin .dll file."); -} - boost::filesystem::path generate_group_endpoint( const std::string& group_name, const boost::filesystem::path& wine_prefix, - const PluginArchitecture architecture) { + const LibArchitecture architecture) { std::ostringstream socket_name; socket_name << "yabridge-group-" << group_name << "-" << std::to_string( std::hash{}(wine_prefix.string())) << "-"; switch (architecture) { - case PluginArchitecture::vst_32: + case LibArchitecture::dll_32: socket_name << "x32"; break; - case PluginArchitecture::vst_64: + case LibArchitecture::dll_64: socket_name << "x64"; break; } @@ -191,24 +334,6 @@ std::vector get_augmented_search_path() { return search_path; } -fs::path get_this_file_location() { - // HACK: Not sure why, but `boost::dll::this_line_location()` returns a path - // starting with a double slash on some systems. I've seen this happen - // 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. 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().starts_with("//")) { - this_file = ("/" / this_file).lexically_normal(); - } - - return this_file; -} - std::string get_wine_version() { // The '*.exe' scripts generated by winegcc allow you to override the binary // used to run Wine, so will will respect this as well @@ -242,17 +367,6 @@ std::string get_wine_version() { return version_string; } -std::string join_quoted_strings(std::vector& strings) { - bool is_first = true; - std::ostringstream joined_strings{}; - for (const auto& option : strings) { - joined_strings << (is_first ? "'" : ", '") << option << "'"; - is_first = false; - } - - return joined_strings.str(); -} - Configuration load_config_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 @@ -264,19 +378,3 @@ Configuration load_config_for(const fs::path& yabridge_path) { return Configuration(*config_file, yabridge_path); } - -bp::environment set_wineprefix() { - bp::environment env = boost::this_process::environment(); - - // Allow the wine prefix to be overridden manually - if (!env["WINEPREFIX"].empty()) { - return env; - } - - const auto wineprefix_path = find_wineprefix(); - if (wineprefix_path) { - env["WINEPREFIX"] = wineprefix_path->string(); - } - - return env; -} diff --git a/src/plugin/utils.h b/src/plugin/utils.h index f11d14e8..1ad98d81 100644 --- a/src/plugin/utils.h +++ b/src/plugin/utils.h @@ -16,11 +16,13 @@ #pragma once -#include +#include + #include #include #include "../common/configuration.h" +#include "../common/plugins.h" /** * Boost 1.72 was released with a known breaking bug caused by a missing @@ -40,10 +42,132 @@ class patched_async_pipe : public boost::process::async_pipe { }; /** - * A tag to differentiate between 32 and 64-bit plugins, used to determine which - * host application to use. + * Marker struct for when we use the default Wine prefix. */ -enum class PluginArchitecture { vst_32, vst_64 }; +struct DefaultWinePrefix {}; + +/** + * Marker struct for when the Wine prefix is overriden using the `WINEPREFIX` + * environment variable. + */ +struct OverridenWinePrefix { + boost::filesystem::path value; +}; + +/** + * This will locate the plugin we're going to host based on the location of the + * `.so` that we're currently operating from and provides information and + * utility functions based on that. + */ +struct PluginInfo { + public: + /** + * Locate the Windows plugin based on the location of this copy of + * `libyabridge-{vst2,vst3}.so` file and the type of the plugin we're going + * to load. For VST2 plugins this is a file with the same name but with a + * `.dll` file extension instead of `.so`. In case this file does not exist + * and the `.so` file is a symlink, we'll also repeat this check for the + * file it links to. This is to support the workflow described in issue #3 + * where you use symlinks to copies of `libyabridge-vst2.so`. + * + * For VST3 plugins there is a strict format as defined by Steinberg, and + * we'll have yabridgectl create a 'merged bundle' that also contains the + * Windows VST3 plugin. + * + * TODO: At the moment we can't choose to use the 32-bit VST3 if a 64-bit + * plugin exists. Potential solutions are to add a config option to + * use the 32-bit version, or we can add a filename suffix to all + * 32-bit versions so they can live alongside each other. + * + * @param plugin_type The type of the plugin we're going to load. The + * detection works slightly differently depending on the plugin type. + * + * @throw std::runtime_error If we cannot find a corresponding Windows + * plugin. The error message contains a human readable description of what + * went wrong. + */ + PluginInfo(PluginType plugin_type); + + /** + * Create the environment for the plugin host based on `wine_prefix`. If + * `WINEPREFIX` was already set then nothing will be changed. Otherwise + * we'll set `WINEPREFIX` to the detected Wine prefix, or it will be left + * unset if we could not detect a prefix. + */ + boost::process::environment create_host_env() const; + + /** + * Return the path to the actual Wine prefix in use, taking into account + * `WINEPREFIX` overrides and the default `~/.wine` fallback. + */ + boost::filesystem::path normalize_wine_prefix() const; + + const PluginType plugin_type; + + /** + * The path to our `.so` file. For VST3 plugins this is *not* the VST3 + * module (since that has to be bundle on Linux) but rather the .so file + * contained in that bundle. + */ + const boost::filesystem::path native_library_path; + + private: + /** + * The path to the Windows library (`.dll` or `.vst3`, not to be confused + * with a `.vst3` bundle) that we're targeting. This should **not** be + * passed to the plugin host and `windows_plugin_path` should be used + * instead. We store this intermediate value so we can determine the + * plugin's architecture. + */ + const boost::filesystem::path windows_library_path; + + public: + const LibArchitecture plugin_arch; + + /** + * The path to the plugin (`.dll` or module) we're going to in the Wine + * plugin host. + * + * For VST2 plugins this will be a `.dll` file. For VST3 plugins this is + * normally a directory called `MyPlugin.vst3` that contains + * `MyPlugin.vst3/Contents/x86-win/MyPlugin.vst3`, but there's also an older + * deprecated (but still ubiquitous) format where the top level + * `MyPlugin.vst3` is not a directory but a .dll file. This points to either + * of those things, and then `VST3::Hosting::Win32Module::create()` will be + * able to load it. + * + * https://developer.steinberg.help/pages/viewpage.action?pageId=9798275 + */ + const boost::filesystem::path windows_plugin_path; + + /** + * The Wine prefix to use for hosting `windows_plugin_path`. If the + * `WINEPREFIX` environment variable is set, then that will be used as an + * override. Otherwise, we'll try to find the Wine prefix + * `windows_plugin_path` is located in. The detection works by looking for a + * directory containing a directory called `dosdevices`. If the plugin is + * not inside of a Wine prefix, this will be left empty, and the default + * prefix will be used instead. + */ + const std:: + variant + wine_prefix; +}; + +/** + * Returns equality for two strings when ignoring casing. Used for comparing + * filenames inside of Wine prefixes since Windows/Wine does case folding for + * filenames. + */ +bool equals_case_insensitive(const std::string& a, const std::string& b); + +/** + * Join a vector of strings with commas while wrapping the strings in quotes. + * For example, `join_quoted_strings(std::vector{"string", "another + * string", "also a string"})` outputs `"'string', 'another string', 'also a + * string'"`. This is used to format the initialisation message. + */ +std::string join_quoted_strings(std::vector& strings); /** * Create a logger prefix based on the endpoint base directory used for the @@ -58,31 +182,19 @@ enum class PluginArchitecture { vst_32, vst_64 }; std::string create_logger_prefix( const boost::filesystem::path& endpoint_base_dir); -/** - * Determine the architecture of a VST plugin (or rather, a .dll file) based on - * it's header values. - * - * See https://docs.microsoft.com/en-us/windows/win32/debug/pe-format for more - * information on the PE32 format. - * - * @param plugin_path The path to the .dll file we're going to check. - * - * @return The detected architecture. - * @throw std::runtime_error If the file is not a .dll file. - */ -PluginArchitecture find_vst_architecture(boost::filesystem::path); - /** * Finds the Wine VST host (either `yabridge-host.exe` or `yabridge-host.exe` * depending on the plugin). For this we will search in two places: * - * 1. Alongside libyabridge.so if the file got symlinked. This is useful - * when developing, as you can simply symlink the the libyabridge.so - * file in the build directory without having to install anything to - * /usr. + * 1. Alongside libyabridge-{vst2,vst3}.so if the file got symlinked. This is + * useful when developing, as you can simply symlink the + * `libyabridge-{vst2,vst3}.so` file in the build directory without having + * to install anything to /usr. * 2. In the regular search path, augmented with `~/.local/share/yabridge` to * ease the setup process. * + * @param this_plugin_path The path to the `.so` file this code is being run + * from. * @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 @@ -90,32 +202,13 @@ PluginArchitecture find_vst_architecture(boost::filesystem::path); * * @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, - bool use_plugin_groups); - -/** - * Find the VST plugin .dll file that corresponds to this copy of - * `libyabridge.so`. This should be the same as the name of this file but with a - * `.dll` file extension instead of `.so`. In case this file does not exist and - * the `.so` file is a symlink, we'll also repeat this check for the file it - * links to. This is to support the workflow described in issue #3 where you use - * symlinks to copies of `libyabridge.so`. * - * @return The a path to the accompanying VST plugin .dll file. - * @throw std::runtime_error If no matching .dll file could be found. + * TODO: Perhaps also move this somewhere else */ -boost::filesystem::path find_vst_plugin(); - -/** - * Locate the Wine prefix this file is located in, if it is inside of a wine - * 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 find_wineprefix(); +boost::filesystem::path find_vst_host( + const boost::filesystem::path& this_plugin_path, + LibArchitecture plugin_arch, + bool use_plugin_groups); /** * Generate the group socket endpoint name used based on the name of the group, @@ -128,21 +221,17 @@ std::optional find_wineprefix(); * * @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. + * obtained from `PluginInfo::normalize_wine_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. + * above. */ boost::filesystem::path generate_group_endpoint( const std::string& group_name, const boost::filesystem::path& wine_prefix, - const PluginArchitecture architecture); + const LibArchitecture architecture); /** * Return the search path as defined in `$PATH`, with `~/.local/share/yabridge` @@ -156,7 +245,7 @@ std::vector get_augmented_search_path(); /** * Return a path to this `.so` file. This can be used to find out from where - * this link to or copy of `libyabridge.so` was loaded. + * this link to or copy of `libyabridge-{vst2,vst3}.so` was loaded. */ boost::filesystem::path get_this_file_location(); @@ -170,20 +259,12 @@ boost::filesystem::path get_this_file_location(); */ std::string get_wine_version(); -/** - * Join a vector of strings with commas while wrapping the strings in quotes. - * For example, `join_quoted_strings(std::vector{"string", "another - * string", "also a string"})` outputs `"'string', 'another string', 'also a - * string'"`. This is used to format the initialisation message. - */ -std::string join_quoted_strings(std::vector& strings); - /** * 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. See the docstrong - * on the `Configuration` class for more details on how to choose the config - * file to load. + * `libyabridge-{vst2,vst3}.so`. If no configuration file could be found then + * this will return an empty configuration object with default settings. See the + * docstrong on the `Configuration` class for more details on how to choose the + * config file to load. * * This function will take any optional compile-time features that have not been * enabled into account. @@ -201,13 +282,6 @@ std::string join_quoted_strings(std::vector& strings); */ Configuration load_config_for(const boost::filesystem::path& yabridge_path); -/** - * Locate the Wine prefix and set the `WINEPREFIX` environment variable if - * found. This way it's also possible to run .dll files outside of a Wine prefix - * 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`. diff --git a/src/plugin/plugin.cpp b/src/plugin/vst2-plugin.cpp similarity index 81% rename from src/plugin/plugin.cpp rename to src/plugin/vst2-plugin.cpp index ac35d2ca..50c17c4c 100644 --- a/src/plugin/plugin.cpp +++ b/src/plugin/vst2-plugin.cpp @@ -19,25 +19,25 @@ #include #include -#include "../common/logging.h" -#include "plugin-bridge.h" +#include "../common/logging/common.h" +#include "bridges/vst2.h" #define VST_EXPORT __attribute__((visibility("default"))) -// The main entry point for VST plugins should be called `VSTPluginMain``. The +// The main entry point for VST2 plugins should be called `VSTPluginMain``. The // other one exist for legacy reasons since some old hosts might still use them. // There's also another possible legacy entry point just called `main`, but GCC // will refuse to compile a function called `main` that's not a regular C++ main -// function +// function. /** - * The main VST plugin entry point. We first set up a bridge that connects to a - * Wine process that hosts the Windows VST plugin. We then create and return a + * The main VST2 plugin entry point. We first set up a bridge that connects to a + * Wine process that hosts the Windows VST2 plugin. We then create and return a * VST plugin struct that acts as a passthrough to the bridge. * * To keep this somewhat contained this is the only place where we're doing * manual memory management. Clean up is done when we receive the `effClose` - * opcode from the VST host (i.e. opcode 1).` + * opcode from the VST2 host (i.e. opcode 1).` */ extern "C" VST_EXPORT AEffect* VSTPluginMain( audioMasterCallback host_callback) { @@ -45,7 +45,7 @@ extern "C" VST_EXPORT AEffect* VSTPluginMain( // This is the only place where we have to use manual memory management. // The bridge's destructor is called when the `effClose` opcode is // received. - PluginBridge* bridge = new PluginBridge(host_callback); + Vst2PluginBridge* bridge = new Vst2PluginBridge(host_callback); return &bridge->plugin; } catch (const std::exception& error) { diff --git a/src/plugin/vst3-plugin.cpp b/src/plugin/vst3-plugin.cpp new file mode 100644 index 00000000..c0955ef8 --- /dev/null +++ b/src/plugin/vst3-plugin.cpp @@ -0,0 +1,72 @@ +// 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 . + +#include "bridges/vst3.h" + +#include + +// Because VST3 plugins consist of completely independent components that have +// to be initialized and connected by the host, hosting a VST3 plugin through +// yabridge works very differently from hosting VST2 plugin. Even with +// individually hosted plugins, all instances of the plugin will be handled by a +// single dynamic library (that VST3 calls a 'module'). Because of this, we'll +// spawn our host process when the first instance of a plugin gets initialized, +// and when the last instance exits so will the host process. +// +// Even though the new VST3 module format where everything's inside of a bundle +// is not particularly common, it is the only standard for Linux and that's what +// we'll use. The installation format for yabridge will thus have the Windows +// plugin symlinked to either the `x86_64-win` or the `x86-win` directory inside +// of the bundle, even if it does not come in a bundle itself. + +Vst3PluginBridge* bridge = nullptr; + +bool InitModule() { + assert(bridge == nullptr); + + try { + bridge = new Vst3PluginBridge(); + + return true; + } catch (const std::exception& error) { + Logger logger = Logger::create_from_environment(); + logger.log("Error during initialization:"); + logger.log(error.what()); + + return false; + } +} + +bool DeinitModule() { + assert(bridge != nullptr); + + delete bridge; + bridge = nullptr; + + return true; +} + +/** + * Our VST3 plugin's entry point. When building the plugin factory we'll host + * the plugin in our Wine application, retrieve its information and supported + * classes, and then recreate it here. + */ +SMTG_EXPORT_SYMBOL Steinberg::IPluginFactory* PLUGIN_API GetPluginFactory() { + // The host should have called `InitModule()` first + assert(bridge); + + return bridge->get_plugin_factory(); +} diff --git a/src/wine-host/boost-fix.h b/src/wine-host/boost-fix.h index bd82b480..478aaeef 100644 --- a/src/wine-host/boost-fix.h +++ b/src/wine-host/boost-fix.h @@ -38,6 +38,7 @@ // included here, but including headers from the detail directory directly // didn't sound like a great idea. +#include #include // #include diff --git a/src/wine-host/bridges/common.cpp b/src/wine-host/bridges/common.cpp new file mode 100644 index 00000000..2aeda267 --- /dev/null +++ b/src/wine-host/bridges/common.cpp @@ -0,0 +1,20 @@ +// 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 . + +#include "common.h" + +HostBridge::HostBridge(boost::filesystem::path plugin_path) + : plugin_path(plugin_path) {} diff --git a/src/wine-host/bridges/common.h b/src/wine-host/bridges/common.h new file mode 100644 index 00000000..d9176725 --- /dev/null +++ b/src/wine-host/bridges/common.h @@ -0,0 +1,76 @@ +// 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 . + +#pragma once + +#include "../boost-fix.h" + +#include + +/** + * The base for the Wine plugin host bridge interface for all plugin types. This + * only has to be able to handle Win32 and X11 events. Implementations of this + * will actually host a plugin and do all the function call forwarding. + */ +class HostBridge { + protected: + HostBridge(boost::filesystem::path plugin_path); + + public: + virtual ~HostBridge(){}; + + /** + * Handle events until the plugin exits. The actual events are posted to + * `main_context` to ensure that all operations to could potentially + * interact with Win32 code are run from a single thread, even when hosting + * multiple plugins. The message loop should be run on a timer within the + * same IO context. + * + * @note Because of the reasons mentioned above, for this to work the plugin + * should be initialized within the same thread that calls + * `main_context.run()`. + */ + virtual void run() = 0; + + /** + * Handle X11 events for the editor window if it is open. This can safely be + * run from any thread. + */ + virtual void handle_x11_events() = 0; + + /** + * Run the message loop for this plugin. This is only used for the + * individual plugin host, so that we can filter out some unnecessary timer + * events. When hosting multiple plugins, a simple central message loop + * should be used instead. This is run on a timer in the same IO context as + * the one that handles the events, i.e. `main_context`. + * + * 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. + */ + virtual void handle_win32_events() = 0; + + /** + * The path to the .dll being loaded in the Wine plugin host. + */ + const boost::filesystem::path plugin_path; +}; diff --git a/src/wine-host/bridges/group.cpp b/src/wine-host/bridges/group.cpp index 8cb528f1..e982f557 100644 --- a/src/wine-host/bridges/group.cpp +++ b/src/wine-host/bridges/group.cpp @@ -16,12 +16,18 @@ #include "group.h" +#include "../boost-fix.h" + #include #include #include #include -#include "../../common/communication.h" +#include "../../common/communication/common.h" +#include "vst2.h" +#ifdef WITH_VST3 +#include "vst3.h" +#endif // FIXME: `std::filesystem` is broken in wineg++, at least under Wine 5.8. Any // path operation will thrown an encoding related error @@ -105,10 +111,10 @@ GroupBridge::~GroupBridge() { stdio_context.stop(); } -void GroupBridge::handle_plugin_dispatch(size_t plugin_id, Vst2Bridge* bridge) { +void GroupBridge::handle_plugin_run(size_t plugin_id, HostBridge* bridge) { // Blocks this thread until the plugin shuts down - bridge->handle_dispatch(); - logger.log("'" + bridge->vst_plugin_path.string() + "' has exited"); + bridge->run(); + logger.log("'" + bridge->plugin_path.string() + "' has exited"); // After the plugin has exited we'll remove this thread's plugin from the // active plugins. This is done within the IO context because the call to @@ -116,7 +122,7 @@ void GroupBridge::handle_plugin_dispatch(size_t plugin_id, Vst2Bridge* bridge) { // potentially corrupt our heap. This way we can also properly join the // thread again. If no active plugins remain, then we'll terminate the // process. - boost::asio::post(main_context.context, [this, plugin_id]() { + main_context.schedule_task([this, plugin_id]() { std::lock_guard lock(active_plugins_mutex); // The join is implicit because we're using Win32Thread (which mimics @@ -176,19 +182,43 @@ void GroupBridge::accept_requests() { // 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(socket); - write_object(socket, GroupResponse{boost::this_process::get_id()}); + const auto request = read_object(socket); + write_object(socket, HostResponse{boost::this_process::get_id()}); // 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 + + logger.log("Received request to host " + + plugin_type_to_string(request.plugin_type) + + " plugin at '" + request.plugin_path + "' using socket endpoint base directory '" + request.endpoint_base_dir + "'"); try { - auto bridge = std::make_unique( - main_context, request.plugin_path, - request.endpoint_base_dir); + std::unique_ptr bridge = nullptr; + switch (request.plugin_type) { + case PluginType::vst2: + bridge = std::make_unique( + main_context, request.plugin_path, + request.endpoint_base_dir); + break; + case PluginType::vst3: +#ifdef WITH_VST3 + bridge = std::make_unique( + main_context, request.plugin_path, + request.endpoint_base_dir); +#else + throw std::runtime_error( + "This version of yabridge has not been compiled " + "with VST3 support"); +#endif + break; + case PluginType::unknown: + throw std::runtime_error( + "Invalid plugin host request received, how did you " + "even manage to do this?"); + break; + } + logger.log("Finished initializing '" + request.plugin_path + "'"); @@ -206,7 +236,7 @@ void GroupBridge::accept_requests() { const size_t plugin_id = next_plugin_id.fetch_add(1); active_plugins[plugin_id] = std::pair( Win32Thread([this, plugin_id, plugin_ptr = bridge.get()]() { - handle_plugin_dispatch(plugin_id, plugin_ptr); + handle_plugin_run(plugin_id, plugin_ptr); }), std::move(bridge)); } catch (const std::runtime_error& error) { diff --git a/src/wine-host/bridges/group.h b/src/wine-host/bridges/group.h index 5a849b56..f72e4938 100644 --- a/src/wine-host/bridges/group.h +++ b/src/wine-host/bridges/group.h @@ -18,6 +18,7 @@ #include "../boost-fix.h" +#include #include #include #include @@ -25,7 +26,9 @@ #include #include -#include "vst2.h" +#include "../common/logging/common.h" +#include "../utils.h" +#include "common.h" /** * Encapsulate capturing the STDOUT or STDERR stream by opening a pipe and @@ -146,7 +149,7 @@ class GroupBridge { * then the process will never exit on its own. This should not happen * though. */ - void handle_plugin_dispatch(size_t plugin_id, Vst2Bridge* bridge); + void handle_plugin_run(size_t plugin_id, HostBridge* bridge); /** * Listen for new requests to spawn plugins within this process and handle @@ -162,14 +165,14 @@ class GroupBridge { * 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 + * running `handle_plugin_run()`. 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()` function to run events within the same * `main_context`. * - * @see handle_plugin_dispatch + * @see handle_plugin_run */ void accept_requests(); @@ -251,7 +254,7 @@ class GroupBridge { * on `next_plugin_id`. */ std::unordered_map>> + std::pair>> active_plugins; /** * A counter for the next unique plugin ID. When hosting a new plugin we'll @@ -261,7 +264,7 @@ class GroupBridge { std::atomic_size_t next_plugin_id; /** * A mutex to prevent two threads from simultaneously accessing the plugins - * map, and also to prevent `handle_plugin_dispatch()` from terminating the + * map, and also to prevent `handle_plugin_run()` from terminating the * process because it thinks there are no active plugins left just as a new * plugin is being spawned. */ @@ -272,7 +275,7 @@ class GroupBridge { * scanning without having to start a new group host process for each * plugin. * - * @see handle_plugin_dispatch + * @see handle_plugin_run */ boost::asio::steady_timer shutdown_timer; /** diff --git a/src/wine-host/bridges/vst2.cpp b/src/wine-host/bridges/vst2.cpp index 7d11aa32..4ef03eed 100644 --- a/src/wine-host/bridges/vst2.cpp +++ b/src/wine-host/bridges/vst2.cpp @@ -16,12 +16,10 @@ #include "vst2.h" -#include -#include #include #include -#include "../../common/communication.h" +#include "../../common/communication/vst2.h" /** * A function pointer to what should be the entry point of a VST plugin. @@ -42,10 +40,12 @@ std::mutex current_bridge_instance_mutex; /** * Opcodes that should always be handled on the main thread because they may * involve GUI operations. + * + * XXX: We removed effEditIdle from here and everything still seems to work + * fine. Verify that this didn't break any plugins. */ const std::set unsafe_opcodes{effOpen, effClose, effEditGetRect, - effEditOpen, effEditClose, effEditIdle, - effEditTop}; + effEditOpen, effEditClose, effEditTop}; intptr_t VST_CALL_CONV host_callback_proxy(AEffect*, int, int, intptr_t, void*, float); @@ -69,7 +69,7 @@ Vst2Bridge& get_bridge_instance(const AEffect* plugin) { Vst2Bridge::Vst2Bridge(MainContext& main_context, std::string plugin_dll_path, std::string endpoint_base_dir) - : vst_plugin_path(plugin_dll_path), + : HostBridge(plugin_dll_path), main_context(main_context), plugin_handle(LoadLibrary(plugin_dll_path.c_str()), FreeLibrary), sockets(main_context.context, endpoint_base_dir, false) { @@ -100,7 +100,7 @@ Vst2Bridge::Vst2Bridge(MainContext& main_context, sockets.connect(); // Initialize after communication has been set up - // We'll try to do the same `get_bridge_isntance` trick as in + // We'll try to do the same `get_bridge_instance` 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. { @@ -271,7 +271,7 @@ Vst2Bridge::Vst2Bridge(MainContext& main_context, }); } -void Vst2Bridge::handle_dispatch() { +void Vst2Bridge::run() { sockets.host_vst_dispatch.receive_events( std::nullopt, [&](Event& event, bool /*on_main_thread*/) { if (event.opcode == effProcessEvents) { @@ -331,15 +331,13 @@ void Vst2Bridge::handle_dispatch() { // instantiated and where the Win32 message loop is // handled. if (unsafe_opcodes.contains(opcode)) { - std::promise dispatch_result; - boost::asio::dispatch(main_context.context, [&]() { - const intptr_t result = dispatch_wrapper( - plugin, opcode, index, value, data, option); - - dispatch_result.set_value(result); - }); - - return dispatch_result.get_future().get(); + return main_context + .run_in_context([&]() { + return dispatch_wrapper(plugin, opcode, + index, value, data, + option); + }) + .get(); } else { return dispatch_wrapper(plugin, opcode, index, value, data, option); @@ -350,6 +348,23 @@ void Vst2Bridge::handle_dispatch() { }); } +void Vst2Bridge::handle_x11_events() { + if (editor) { + editor->handle_x11_events(); + } +} + +void Vst2Bridge::handle_win32_events() { + MSG msg; + + for (int i = 0; + i < max_win32_messages && PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE); + i++) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin, int opcode, int index, @@ -370,7 +385,7 @@ intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin, const std::string window_class = "yabridge plugin " + sockets.base_dir.string(); Editor& editor_instance = - editor.emplace(config, window_class, x11_handle, plugin); + editor.emplace(config, window_class, x11_handle); return plugin->dispatcher(plugin, opcode, index, value, editor_instance.get_win32_handle(), @@ -392,27 +407,6 @@ intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin, } } -void Vst2Bridge::handle_win32_events() { - if (editor) { - editor->handle_win32_events(); - } else { - MSG msg; - - for (int i = 0; i < max_win32_messages && - PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE); - i++) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - } -} - -void Vst2Bridge::handle_x11_events() { - if (editor) { - editor->handle_x11_events(); - } -} - class HostCallbackDataConverter : DefaultDataConverter { public: HostCallbackDataConverter(AEffect* plugin, @@ -462,7 +456,7 @@ class HostCallbackDataConverter : DefaultDataConverter { switch (opcode) { case audioMasterGetTime: // Write the returned `VstTimeInfo` struct into a field and - // make the function return a poitner to it in the function + // make the function return a pointer 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. diff --git a/src/wine-host/bridges/vst2.h b/src/wine-host/bridges/vst2.h index c3e1ed16..23426289 100644 --- a/src/wine-host/bridges/vst2.h +++ b/src/wine-host/bridges/vst2.h @@ -18,46 +18,34 @@ #include "../boost-fix.h" +#ifndef NOMINMAX #define NOMINMAX -#define NOSERVICE -#define NOMCX -#define NOIMM -#define WIN32_LEAN_AND_MEAN +#define WINE_NOWINSOCK +#endif #include #include -#include -#include - -#include "../../common/communication.h" +#include "../../common/communication/vst2.h" #include "../../common/configuration.h" -#include "../../common/logging.h" #include "../editor.h" #include "../utils.h" +#include "common.h" /** * 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 a single thread. Most plugins won't have any issues when using - * multiple message loops, but the Melda plugins for instance will only update - * their GUIs from the message loop of the thread that created the first - * instance. This is why we pass an IO context to this class so everything - * that's not performance critical (audio and midi event handling) is handled - * on the same thread, even when hosting multiple plugins. */ -class Vst2Bridge { +class Vst2Bridge : public HostBridge { public: /** - * Initializes the Windows VST plugin and set up communication with the - * native Linux VST plugin. + * Initializes the Windows VST2 plugin and set up communication with the + * native Linux VST2 plugin. * * @param main_context The main IO context for this application. Most events * will be dispatched to this context, and the event handling loop should * also be run from this context. - * @param plugin_dll_path A (Unix style) path to the VST plugin .dll file to - * load. + * @param plugin_dll_path A (Unix style) path to the VST2 plugin .dll file + * to load. * @param endpoint_base_dir The base directory used for the socket * endpoints. See `Sockets` for more information. * @@ -72,41 +60,13 @@ class Vst2Bridge { std::string endpoint_base_dir); /** - * Handle events until the plugin exits. The actual events are posted to - * `main_context` to ensure that all operations to could potentially - * interact with Win32 code are run from a single thread, even when hosting - * multiple plugins. The message loop should be run on a timer within the - * same IO context. - * - * @note Because of the reasons mentioned above, for this to work the plugin - * should be initialized within the same thread that calls - * `main_context.run()`. + * Here we'll handle incoming `dispatch()` messages until the sockets get + * closed during `effClose()`. */ - void handle_dispatch(); + void run() override; - /** - * Handle X11 events for the editor window if it is open. This can safely be - * run from any thread. - */ - void handle_x11_events(); - - /** - * Run the message loop for this plugin. This is only used for the - * individual plugin host, so that we can filter out some unnecessary timer - * events. When hosting multiple plugins, a simple central message loop - * should be used instead. This is run on a timer in the same IO context as - * the one that handles the events, i.e. `main_context`. - * - * 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. - */ - void handle_win32_events(); + void handle_x11_events() override; + void handle_win32_events() override; /** * Forward the host callback made by the plugin to the host and return the @@ -122,11 +82,6 @@ class Vst2Bridge { */ std::optional time_info; - /** - * The path to the .dll being loaded in the Wine VST host. - */ - const boost::filesystem::path vst_plugin_path; - private: /** * A wrapper around `plugin->dispatcher` that handles the opening and @@ -189,7 +144,14 @@ class Vst2Bridge { * sockets will be closed first, and we can then safely wait for the * threads to exit. */ - Sockets sockets; + Vst2Sockets sockets; + + /** + * The plugin editor window. Allows embedding the plugin's editor into a + * Wine window, and embedding that Wine window into a window provided by the + * host. Should be empty when the editor is not open. + */ + std::optional editor; /** * The MIDI events that have been received **and processed** since the last @@ -204,13 +166,4 @@ class Vst2Bridge { * now happens in two different threads. */ std::mutex next_buffer_midi_events_mutex; - - /** - * The plugin editor window. Allows embedding the plugin's editor into a - * Wine window, and embedding that Wine window into a window provided by the - * host. Should be empty when the editor is not open. - * - * @see should_postpone_message_loop - */ - std::optional editor; }; diff --git a/src/wine-host/bridges/vst3-impls/component-handler-proxy.cpp b/src/wine-host/bridges/vst3-impls/component-handler-proxy.cpp new file mode 100644 index 00000000..ece96996 --- /dev/null +++ b/src/wine-host/bridges/vst3-impls/component-handler-proxy.cpp @@ -0,0 +1,83 @@ +// 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 . + +#include "component-handler-proxy.h" + +#include + +Vst3ComponentHandlerProxyImpl::Vst3ComponentHandlerProxyImpl( + Vst3Bridge& bridge, + Vst3ComponentHandlerProxy::ConstructArgs&& args) + : Vst3ComponentHandlerProxy(std::move(args)), bridge(bridge) { + // The lifecycle of this object is managed together with that of the plugin + // object instance this host context got passed to +} + +tresult PLUGIN_API +Vst3ComponentHandlerProxyImpl::queryInterface(const Steinberg::TUID _iid, + void** obj) { + // TODO: Successful queries should also be logged + const tresult result = Vst3ComponentHandlerProxy::queryInterface(_iid, obj); + if (result != Steinberg::kResultOk) { + bridge.logger.log_unknown_interface( + "In IComponentHandler::queryInterface()", + Steinberg::FUID::fromTUID(_iid)); + } + + return result; +} + +tresult PLUGIN_API +Vst3ComponentHandlerProxyImpl::beginEdit(Steinberg::Vst::ParamID id) { + return bridge.send_message(YaComponentHandler::BeginEdit{ + .owner_instance_id = owner_instance_id(), .id = id}); +} + +tresult PLUGIN_API Vst3ComponentHandlerProxyImpl::performEdit( + Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue valueNormalized) { + return bridge.send_message(YaComponentHandler::PerformEdit{ + .owner_instance_id = owner_instance_id(), + .id = id, + .value_normalized = valueNormalized}); +} + +tresult PLUGIN_API +Vst3ComponentHandlerProxyImpl::endEdit(Steinberg::Vst::ParamID id) { + return bridge.send_message(YaComponentHandler::EndEdit{ + .owner_instance_id = owner_instance_id(), .id = id}); +} + +tresult PLUGIN_API +Vst3ComponentHandlerProxyImpl::restartComponent(int32 flags) { + return bridge.send_message(YaComponentHandler::RestartComponent{ + .owner_instance_id = owner_instance_id(), .flags = flags}); +} + +tresult PLUGIN_API Vst3ComponentHandlerProxyImpl::notifyUnitSelection( + Steinberg::Vst::UnitID unitId) { + return bridge.send_message(YaUnitHandler::NotifyUnitSelection{ + .owner_instance_id = owner_instance_id(), .unit_id = unitId}); +} + +tresult PLUGIN_API Vst3ComponentHandlerProxyImpl::notifyProgramListChange( + Steinberg::Vst::ProgramListID listId, + int32 programIndex) { + return bridge.send_message(YaUnitHandler::NotifyProgramListChange{ + .owner_instance_id = owner_instance_id(), + .list_id = listId, + .program_index = programIndex}); +} diff --git a/src/wine-host/bridges/vst3-impls/component-handler-proxy.h b/src/wine-host/bridges/vst3-impls/component-handler-proxy.h new file mode 100644 index 00000000..fcf0edf9 --- /dev/null +++ b/src/wine-host/bridges/vst3-impls/component-handler-proxy.h @@ -0,0 +1,51 @@ +// 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 . + +#pragma once + +#include "../vst3.h" + +class Vst3ComponentHandlerProxyImpl : public Vst3ComponentHandlerProxy { + public: + Vst3ComponentHandlerProxyImpl( + Vst3Bridge& bridge, + Vst3ComponentHandlerProxy::ConstructArgs&& args); + + /** + * We'll override the query interface to log queries for interfaces we do + * not (yet) support. + */ + tresult PLUGIN_API queryInterface(const Steinberg::TUID _iid, + void** obj) override; + + // From `IComponentHandler` + tresult PLUGIN_API beginEdit(Steinberg::Vst::ParamID id) override; + tresult PLUGIN_API + performEdit(Steinberg::Vst::ParamID id, + Steinberg::Vst::ParamValue valueNormalized) override; + tresult PLUGIN_API endEdit(Steinberg::Vst::ParamID id) override; + tresult PLUGIN_API restartComponent(int32 flags) override; + + // From `IUnitHandler` + tresult PLUGIN_API + notifyUnitSelection(Steinberg::Vst::UnitID unitId) override; + tresult PLUGIN_API + notifyProgramListChange(Steinberg::Vst::ProgramListID listId, + int32 programIndex) override; + + private: + Vst3Bridge& bridge; +}; diff --git a/src/wine-host/bridges/vst3-impls/connection-point-proxy.cpp b/src/wine-host/bridges/vst3-impls/connection-point-proxy.cpp new file mode 100644 index 00000000..4949a1f8 --- /dev/null +++ b/src/wine-host/bridges/vst3-impls/connection-point-proxy.cpp @@ -0,0 +1,68 @@ +// 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 . + +#include "connection-point-proxy.h" + +#include + +Vst3ConnectionPointProxyImpl::Vst3ConnectionPointProxyImpl( + Vst3Bridge& bridge, + Vst3ConnectionPointProxy::ConstructArgs&& args) + : Vst3ConnectionPointProxy(std::move(args)), bridge(bridge) {} + +tresult PLUGIN_API +Vst3ConnectionPointProxyImpl::queryInterface(const Steinberg::TUID _iid, + void** obj) { + // TODO: Successful queries should also be logged + const tresult result = Vst3ConnectionPointProxy::queryInterface(_iid, obj); + if (result != Steinberg::kResultOk) { + bridge.logger.log_unknown_interface( + "In IConnectionPoint::queryInterface()", + Steinberg::FUID::fromTUID(_iid)); + } + + return result; +} + +tresult PLUGIN_API +Vst3ConnectionPointProxyImpl::connect(IConnectionPoint* /*other*/) { + std::cerr << "WARNING: The plugin called IConnectionPoint::connect(), this " + "should not happen" + << std::endl; + return Steinberg::kNotImplemented; +} + +tresult PLUGIN_API +Vst3ConnectionPointProxyImpl::disconnect(IConnectionPoint* /*other*/) { + std::cerr << "WARNING: The plugin called IConnectionPoint::disconnect(), " + "this should not happen" + << std::endl; + return Steinberg::kNotImplemented; +} + +tresult PLUGIN_API +Vst3ConnectionPointProxyImpl::notify(Steinberg::Vst::IMessage* message) { + if (message) { + return bridge.send_message( + YaConnectionPoint::Notify{.instance_id = owner_instance_id(), + .message_ptr = YaMessagePtr(*message)}); + } else { + std::cerr << "WARNING: Null pointer passed to " + "'IConnectionPoint::notify()', ignoring" + << std::endl; + return Steinberg::kInvalidArgument; + } +} diff --git a/src/wine-host/bridges/vst3-impls/connection-point-proxy.h b/src/wine-host/bridges/vst3-impls/connection-point-proxy.h new file mode 100644 index 00000000..9cff5c9e --- /dev/null +++ b/src/wine-host/bridges/vst3-impls/connection-point-proxy.h @@ -0,0 +1,41 @@ +// 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 . + +#pragma once + +#include "../vst3.h" + +class Vst3ConnectionPointProxyImpl : public Vst3ConnectionPointProxy { + public: + Vst3ConnectionPointProxyImpl( + Vst3Bridge& bridge, + Vst3ConnectionPointProxy::ConstructArgs&& args); + + /** + * We'll override the query interface to log queries for interfaces we do + * not (yet) support. + */ + tresult PLUGIN_API queryInterface(const Steinberg::TUID _iid, + void** obj) override; + + // From `IConnectionPoint` + tresult PLUGIN_API connect(IConnectionPoint* other) override; + tresult PLUGIN_API disconnect(IConnectionPoint* other) override; + tresult PLUGIN_API notify(Steinberg::Vst::IMessage* message) override; + + private: + Vst3Bridge& bridge; +}; diff --git a/src/wine-host/bridges/vst3-impls/host-context-proxy.cpp b/src/wine-host/bridges/vst3-impls/host-context-proxy.cpp new file mode 100644 index 00000000..ae572e76 --- /dev/null +++ b/src/wine-host/bridges/vst3-impls/host-context-proxy.cpp @@ -0,0 +1,91 @@ +// 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 . + +#include "host-context-proxy.h" + +#include + +#include "../../../common/serialization/vst3/attribute-list.h" +#include "../../../common/serialization/vst3/message.h" + +Vst3HostContextProxyImpl::Vst3HostContextProxyImpl( + Vst3Bridge& bridge, + Vst3HostContextProxy::ConstructArgs&& args) + : Vst3HostContextProxy(std::move(args)), bridge(bridge) { + // The lifecycle of this object is managed together with that of the plugin + // object instance this host context got passed to +} + +tresult PLUGIN_API +Vst3HostContextProxyImpl::queryInterface(const Steinberg::TUID _iid, + void** obj) { + // I don't think it's expected of a host to implement multiple interfaces on + // this object, so if we do get a call here it's important that it's logged + // TODO: Successful queries should also be logged + const tresult result = Vst3HostContextProxy::queryInterface(_iid, obj); + if (result != Steinberg::kResultOk) { + bridge.logger.log_unknown_interface("In FUnknown::queryInterface()", + Steinberg::FUID::fromTUID(_iid)); + } + + return result; +} + +tresult PLUGIN_API +Vst3HostContextProxyImpl::getName(Steinberg::Vst::String128 name) { + const GetNameResponse response = bridge.send_message( + YaHostApplication::GetName{.owner_instance_id = owner_instance_id()}); + + std::copy(response.name.begin(), response.name.end(), name); + name[response.name.size()] = 0; + + return response.result; +} + +tresult PLUGIN_API +Vst3HostContextProxyImpl::createInstance(Steinberg::TUID /*cid*/, + Steinberg::TUID _iid, + void** obj) { + // Class IDs don't have a meaning here, they just mirrored the interface + // from `IPlugFactory::createInstance()` + constexpr size_t uid_size = sizeof(Steinberg::TUID); + if (!_iid || !obj || strnlen(_iid, uid_size) < uid_size) { + return Steinberg::kInvalidArgument; + } + + // These objects don't have to be created by the actual host since they'll + // only be used as an argument to other functions. We can just serialize + // them at that point. + Steinberg::FUID iid = Steinberg::FUID::fromTUID(_iid); + if (iid == Steinberg::Vst::IMessage::iid) { + // TODO: Add logging for this on verbosity level 1 + *obj = static_cast(new YaMessage{}); + return Steinberg::kResultTrue; + } else if (iid == Steinberg::Vst::IAttributeList::iid) { + // TODO: Add logging for this on verbosity level 1 + *obj = + static_cast(new YaAttributeList{}); + return Steinberg::kResultTrue; + } else { + // When the host requests an interface we do not (yet) implement, + // we'll print a recognizable log message + const Steinberg::FUID uid = Steinberg::FUID::fromTUID(_iid); + bridge.logger.log_unknown_interface( + "In IHostApplication::createInstance()", uid); + + return Steinberg::kNotImplemented; + } +} diff --git a/src/wine-host/bridges/vst3-impls/host-context-proxy.h b/src/wine-host/bridges/vst3-impls/host-context-proxy.h new file mode 100644 index 00000000..85ca3476 --- /dev/null +++ b/src/wine-host/bridges/vst3-impls/host-context-proxy.h @@ -0,0 +1,41 @@ +// 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 . + +#pragma once + +#include "../vst3.h" + +class Vst3HostContextProxyImpl : public Vst3HostContextProxy { + public: + Vst3HostContextProxyImpl(Vst3Bridge& bridge, + Vst3HostContextProxy::ConstructArgs&& args); + + /** + * We'll override the query interface to log queries for interfaces we do + * not (yet) support. + */ + tresult PLUGIN_API queryInterface(const Steinberg::TUID _iid, + void** obj) override; + + // From `IHostApplication` + tresult PLUGIN_API getName(Steinberg::Vst::String128 name) override; + tresult PLUGIN_API createInstance(Steinberg::TUID cid, + Steinberg::TUID _iid, + void** obj) override; + + private: + Vst3Bridge& bridge; +}; diff --git a/src/wine-host/bridges/vst3-impls/plug-frame-proxy.cpp b/src/wine-host/bridges/vst3-impls/plug-frame-proxy.cpp new file mode 100644 index 00000000..74a68021 --- /dev/null +++ b/src/wine-host/bridges/vst3-impls/plug-frame-proxy.cpp @@ -0,0 +1,60 @@ +// 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 . + +#include "plug-frame-proxy.h" + +#include + +Vst3PlugFrameProxyImpl::Vst3PlugFrameProxyImpl( + Vst3Bridge& bridge, + Vst3PlugFrameProxy::ConstructArgs&& args) + : Vst3PlugFrameProxy(std::move(args)), bridge(bridge) { + // The lifecycle of this object is managed together with that of the plugin + // object instance this host context got passed to +} + +tresult PLUGIN_API +Vst3PlugFrameProxyImpl::queryInterface(const Steinberg::TUID _iid, void** obj) { + // TODO: Successful queries should also be logged + const tresult result = Vst3PlugFrameProxy::queryInterface(_iid, obj); + if (result != Steinberg::kResultOk) { + bridge.logger.log_unknown_interface("In IPlugFrame::queryInterface()", + Steinberg::FUID::fromTUID(_iid)); + } + + return result; +} + +tresult PLUGIN_API +Vst3PlugFrameProxyImpl::resizeView(Steinberg::IPlugView* /*view*/, + Steinberg::ViewRect* newSize) { + if (newSize) { + // XXX: Since VST3 currently only support a single view type we'll + // assume `view` is the `IPlugView*` returned by the last call to + // `IEditController::createView()` + + // We have to use this special sending function here so we can handle + // calls to `IPlugView::onSize()` from this same thread (the UI thread). + // See the docstring for more information. + return bridge.send_mutually_recursive_message(YaPlugFrame::ResizeView{ + .owner_instance_id = owner_instance_id(), .new_size = *newSize}); + } else { + std::cerr + << "WARNING: Null pointer passed to 'IPlugFrame::resizeView()'" + << std::endl; + return Steinberg::kInvalidArgument; + } +} diff --git a/src/wine-host/bridges/vst3-impls/plug-frame-proxy.h b/src/wine-host/bridges/vst3-impls/plug-frame-proxy.h new file mode 100644 index 00000000..e15066a6 --- /dev/null +++ b/src/wine-host/bridges/vst3-impls/plug-frame-proxy.h @@ -0,0 +1,39 @@ +// 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 . + +#pragma once + +#include "../vst3.h" + +class Vst3PlugFrameProxyImpl : public Vst3PlugFrameProxy { + public: + Vst3PlugFrameProxyImpl(Vst3Bridge& bridge, + Vst3PlugFrameProxy::ConstructArgs&& args); + + /** + * We'll override the query interface to log queries for interfaces we do + * not (yet) support. + */ + tresult PLUGIN_API queryInterface(const Steinberg::TUID _iid, + void** obj) override; + + // From `IPlugFrame` + tresult PLUGIN_API resizeView(Steinberg::IPlugView* view, + Steinberg::ViewRect* newSize) override; + + private: + Vst3Bridge& bridge; +}; diff --git a/src/wine-host/bridges/vst3.cpp b/src/wine-host/bridges/vst3.cpp new file mode 100644 index 00000000..8e89907d --- /dev/null +++ b/src/wine-host/bridges/vst3.cpp @@ -0,0 +1,920 @@ +// 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 . + +#include "vst3.h" + +#include "../boost-fix.h" + +#include + +#include "vst3-impls/component-handler-proxy.h" +#include "vst3-impls/connection-point-proxy.h" +#include "vst3-impls/host-context-proxy.h" +#include "vst3-impls/plug-frame-proxy.h" + +InstanceInterfaces::InstanceInterfaces() {} + +InstanceInterfaces::InstanceInterfaces( + Steinberg::IPtr object) + : object(object), + audio_processor(object), + component(object), + connection_point(object), + edit_controller(object), + edit_controller_2(object), + plugin_base(object), + unit_data(object), + program_list_data(object), + unit_info(object) {} + +Vst3Bridge::Vst3Bridge(MainContext& main_context, + std::string plugin_dll_path, + std::string endpoint_base_dir) + : HostBridge(plugin_dll_path), + generic_logger(Logger::create_wine_stderr()), + logger(generic_logger), + main_context(main_context), + sockets(main_context.context, endpoint_base_dir, false) { + std::string error; + module = VST3::Hosting::Win32Module::create(plugin_dll_path, error); + if (!module) { + throw std::runtime_error("Could not load the VST3 module for '" + + plugin_dll_path + "': " + error); + } + + sockets.connect(); + + // Fetch this instance's configuration from the plugin to finish the setup + // process + config = sockets.vst_host_callback.send_message(WantsConfiguration{}, + std::nullopt); +} + +void Vst3Bridge::run() { + // XXX: In theory all of thise should be safe assuming the host doesn't do + // anything weird. We're using mutexes when inserting and removing + // things, but for correctness we should have a multiple-readers + // single-writer style lock since concurrent reads and writes can also + // be unsafe. + sockets.host_vst_control.receive_messages( + std::nullopt, + overload{ + [&](const Vst3PlugViewProxy::Destruct& request) + -> Vst3PlugViewProxy::Destruct::Response { + // XXX: Not sure if his has to be run form the UI thread + main_context + .run_in_context([&]() { + // When the pointer gets dropped by the host, we want to + // drop it here as well, along with the `IPlugFrame` + // proxy object it may have received in + // `IPlugView::setFrame()`. + object_instances[request.owner_instance_id] + .plug_view.reset(); + object_instances[request.owner_instance_id] + .plug_frame_proxy.reset(); + }) + .wait(); + + return Ack{}; + }, + [&](const Vst3PluginProxy::Construct& request) + -> Vst3PluginProxy::Construct::Response { + Steinberg::TUID cid; + std::copy(request.cid.begin(), request.cid.end(), cid); + + // Even though we're requesting a specific interface (to mimic + // what the host is doing), we're immediately upcasting it to an + // `FUnknown` so we can create a perfect proxy object. + // We create the object from the GUI thread in case it + // immediatly starts timers or something (even though it + // shouldn't) + Steinberg::IPtr object = + main_context + .run_in_context>( + [&]() -> Steinberg::IPtr { + switch (request.requested_interface) { + case Vst3PluginProxy::Construct::Interface:: + IComponent: + return module->getFactory() + .createInstance< + Steinberg::Vst::IComponent>( + cid); + break; + case Vst3PluginProxy::Construct::Interface:: + IEditController: + return module->getFactory() + .createInstance< + Steinberg::Vst:: + IEditController>(cid); + break; + default: + // Unreachable + return nullptr; + break; + } + }) + .get(); + + if (!object) { + return UniversalTResult(Steinberg::kResultFalse); + } + + const size_t instance_id = register_object_instance(object); + + // This is where the magic happens. Here we deduce which + // interfaces are supported by this object so we can create + // a one-to-one proxy of it. + return Vst3PluginProxy::ConstructArgs( + object_instances[instance_id].object, instance_id); + }, + [&](const Vst3PluginProxy::Destruct& request) + -> Vst3PluginProxy::Destruct::Response { + unregister_object_instance(request.instance_id); + return Ack{}; + }, + [&](Vst3PluginProxy::SetState& request) + -> Vst3PluginProxy::SetState::Response { + // This same function is defined in both `IComponent` and + // `IEditController`, so the host is calling one or the other + if (object_instances[request.instance_id].component) { + return object_instances[request.instance_id] + .component->setState(&request.state); + } else { + return object_instances[request.instance_id] + .edit_controller->setState(&request.state); + } + }, + [&](Vst3PluginProxy::GetState& request) + -> Vst3PluginProxy::GetState::Response { + VectorStream stream{}; + tresult result; + + // This same function is defined in both `IComponent` and + // `IEditController`, so the host is calling one or the other + if (object_instances[request.instance_id].component) { + result = object_instances[request.instance_id] + .component->getState(&stream); + } else { + result = object_instances[request.instance_id] + .edit_controller->getState(&stream); + } + + return Vst3PluginProxy::GetStateResponse{ + .result = result, .updated_state = std::move(stream)}; + }, + [&](YaConnectionPoint::Connect& request) + -> YaConnectionPoint::Connect::Response { + // If the host directly connected the underlying objects then we + // can directly connect them as well. Otherwise we'll have to go + // through a connection proxy (to proxy the host's connection + // proxy). + return std::visit( + overload{ + [&](const native_size_t& other_instance_id) -> tresult { + return object_instances[request.instance_id] + .connection_point->connect( + object_instances[other_instance_id] + .connection_point); + }, + [&](Vst3ConnectionPointProxy::ConstructArgs& args) + -> tresult { + object_instances[request.instance_id] + .connection_point_proxy = Steinberg::owned( + new Vst3ConnectionPointProxyImpl( + *this, std::move(args))); + + return object_instances[request.instance_id] + .connection_point->connect( + object_instances[request.instance_id] + .connection_point_proxy); + }}, + request.other); + }, + [&](const YaConnectionPoint::Disconnect& request) + -> YaConnectionPoint::Disconnect::Response { + // If the objects were connected directly we can also disconnect + // them directly. Otherwise we'll disconnect them from our proxy + // object and then destroy that proxy object. + if (request.other_instance_id) { + return object_instances[request.instance_id] + .connection_point->disconnect( + object_instances[*request.other_instance_id] + .connection_point); + } else { + const tresult result = + object_instances[request.instance_id] + .connection_point->disconnect( + object_instances[*request.other_instance_id] + .connection_point_proxy); + object_instances[*request.other_instance_id] + .connection_point_proxy.reset(); + + return result; + } + }, + [&](const YaConnectionPoint::Notify& request) + -> YaConnectionPoint::Notify::Response { + // NOTE: We're using a few tricks here to pass through a pointer + // to the _original_ `IMessage` object passed to a + // connection proxy. This is needed because some plugins + // like iZotope VocalSynth 2 use these messages to + // exchange pointers between their objects so they can + // break out of VST3's separation, but they might also + // store the message object and not the actual pointers. + // We should thus be passing a (raw) pointer to the + // original object so we can pretend none of this wrapping + // and serializing has ever happened. + return object_instances[request.instance_id] + .connection_point->notify( + request.message_ptr.get_original()); + }, + [&](YaEditController::SetComponentState& request) + -> YaEditController::SetComponentState::Response { + return object_instances[request.instance_id] + .edit_controller->setComponentState(&request.state); + }, + [&](const YaEditController::GetParameterCount& request) + -> YaEditController::GetParameterCount::Response { + return object_instances[request.instance_id] + .edit_controller->getParameterCount(); + }, + [&](YaEditController::GetParameterInfo& request) + -> YaEditController::GetParameterInfo::Response { + const tresult result = + object_instances[request.instance_id] + .edit_controller->getParameterInfo(request.param_index, + request.info); + + return YaEditController::GetParameterInfoResponse{ + .result = result, .updated_info = std::move(request.info)}; + }, + [&](const YaEditController::GetParamStringByValue& request) + -> YaEditController::GetParamStringByValue::Response { + Steinberg::Vst::String128 string{0}; + const tresult result = + object_instances[request.instance_id] + .edit_controller->getParamStringByValue( + request.id, request.value_normalized, string); + + return YaEditController::GetParamStringByValueResponse{ + .result = result, + .string = tchar_pointer_to_u16string(string)}; + }, + [&](const YaEditController::GetParamValueByString& request) + -> YaEditController::GetParamValueByString::Response { + Steinberg::Vst::ParamValue value_normalized; + const tresult result = + object_instances[request.instance_id] + .edit_controller->getParamValueByString( + request.id, + const_cast( + u16string_to_tchar_pointer( + request.string.c_str())), + value_normalized); + + return YaEditController::GetParamValueByStringResponse{ + .result = result, .value_normalized = value_normalized}; + }, + [&](const YaEditController::NormalizedParamToPlain& request) + -> YaEditController::NormalizedParamToPlain::Response { + return object_instances[request.instance_id] + .edit_controller->normalizedParamToPlain( + request.id, request.value_normalized); + }, + [&](const YaEditController::PlainParamToNormalized& request) + -> YaEditController::PlainParamToNormalized::Response { + return object_instances[request.instance_id] + .edit_controller->plainParamToNormalized( + request.id, request.plain_value); + }, + [&](const YaEditController::GetParamNormalized& request) + -> YaEditController::GetParamNormalized::Response { + return object_instances[request.instance_id] + .edit_controller->getParamNormalized(request.id); + }, + [&](const YaEditController::SetParamNormalized& request) + -> YaEditController::SetParamNormalized::Response { + return object_instances[request.instance_id] + .edit_controller->setParamNormalized(request.id, + request.value); + }, + [&](YaEditController::SetComponentHandler& request) + -> YaEditController::SetComponentHandler::Response { + // We'll create a proxy object for the component handler and + // pass that to the initialize function. The lifetime of this + // object is tied to that of the actual plugin object we're + // proxying for. + // TODO: Does this have to be run from the UI thread? Figure out + // if it does + object_instances[request.instance_id].component_handler_proxy = + Steinberg::owned(new Vst3ComponentHandlerProxyImpl( + *this, + std::move(request.component_handler_proxy_args))); + + return object_instances[request.instance_id] + .edit_controller->setComponentHandler( + object_instances[request.instance_id] + .component_handler_proxy); + }, + [&](const YaEditController::CreateView& request) + -> YaEditController::CreateView::Response { + // Instantiate the object from the GUI thread + main_context + .run_in_context([&]() { + object_instances[request.instance_id].plug_view = + Steinberg::owned( + object_instances[request.instance_id] + .edit_controller->createView( + request.name.c_str())); + }) + .wait(); + + // We'll create a proxy so the host can call functions on this + // `IPlugView` object + return YaEditController::CreateViewResponse{ + .plug_view_args = + (object_instances[request.instance_id].plug_view + ? std::make_optional< + Vst3PlugViewProxy::ConstructArgs>( + object_instances[request.instance_id] + .plug_view, + request.instance_id) + : std::nullopt)}; + }, + [&](const YaEditController2::SetKnobMode& request) + -> YaEditController2::SetKnobMode::Response { + return object_instances[request.instance_id] + .edit_controller_2->setKnobMode(request.mode); + }, + [&](const YaEditController2::OpenHelp& request) + -> YaEditController2::OpenHelp::Response { + return object_instances[request.instance_id] + .edit_controller_2->openHelp(request.only_check); + }, + [&](const YaEditController2::OpenAboutBox& request) + -> YaEditController2::OpenAboutBox::Response { + return object_instances[request.instance_id] + .edit_controller_2->openAboutBox(request.only_check); + }, + [&](const YaPlugView::IsPlatformTypeSupported& request) + -> YaPlugView::IsPlatformTypeSupported::Response { + // The host will of course want to pass an X11 window ID for the + // plugin to embed itself in, so we'll have to translate this to + // a HWND + const std::string type = + request.type == Steinberg::kPlatformTypeX11EmbedWindowID + ? Steinberg::kPlatformTypeHWND + : request.type; + + return object_instances[request.owner_instance_id] + .plug_view->isPlatformTypeSupported(type.c_str()); + }, + [&](const YaPlugView::Attached& request) + -> YaPlugView::Attached::Response { + const std::string type = + request.type == Steinberg::kPlatformTypeX11EmbedWindowID + ? Steinberg::kPlatformTypeHWND + : request.type; + + // Just like with VST2 plugins, we'll embed a Wine window into + // the X11 window provided by the host + // TODO: The docs say that we should support XEmbed (and we're + // purposely avoiding that because Wine's implementation + // doesn't work correctly). Check if this causes issues, + // and if it's actually needed (for instance when the host + // resizes the window without informing the plugin) + const auto x11_handle = static_cast(request.parent); + const std::string window_class = + "yabridge plugin " + sockets.base_dir.string() + " " + + std::to_string(request.owner_instance_id); + + // Creating the window and having the plugin embed in it should + // be done in the main UI thread + return main_context + .run_in_context([&]() { + Editor& editor_instance = + object_instances[request.owner_instance_id] + .editor.emplace(config, window_class, + x11_handle); + + const tresult result = + object_instances[request.owner_instance_id] + .plug_view->attached( + editor_instance.get_win32_handle(), + type.c_str()); + + // Get rid of the editor again if the plugin didn't + // embed itself in it + if (result != Steinberg::kResultOk) { + object_instances[request.owner_instance_id] + .editor.reset(); + } + + return result; + }) + .get(); + }, + [&](const YaPlugView::Removed& request) + -> YaPlugView::Removed::Response { + return main_context + .run_in_context([&]() { + const tresult result = + object_instances[request.owner_instance_id] + .plug_view->removed(); + + object_instances[request.owner_instance_id] + .editor.reset(); + + return result; + }) + .get(); + }, + [&](const YaPlugView::OnWheel& request) + -> YaPlugView::OnWheel::Response { + // Since all of these `IPlugView::on*` functions can cause a + // redraw, they all have to be called from the UI thread + return main_context + .run_in_context([&]() { + return object_instances[request.owner_instance_id] + .plug_view->onWheel(request.distance); + }) + .get(); + }, + [&](const YaPlugView::OnKeyDown& request) + -> YaPlugView::OnKeyDown::Response { + return main_context + .run_in_context([&]() { + return object_instances[request.owner_instance_id] + .plug_view->onKeyDown(request.key, request.key_code, + request.modifiers); + }) + .get(); + }, + [&](const YaPlugView::OnKeyUp& request) + -> YaPlugView::OnKeyUp::Response { + return main_context + .run_in_context([&]() { + return object_instances[request.owner_instance_id] + .plug_view->onKeyUp(request.key, request.key_code, + request.modifiers); + }) + .get(); + }, + [&](YaPlugView::GetSize& request) -> YaPlugView::GetSize::Response { + const tresult result = + object_instances[request.owner_instance_id] + .plug_view->getSize(&request.size); + + return YaPlugView::GetSizeResponse{ + .result = result, .updated_size = std::move(request.size)}; + }, + [&](YaPlugView::OnSize& request) -> YaPlugView::OnSize::Response { + // HACK: This function has to be run from the UI thread since + // the plugin probably wants to redraw when it gets + // resized. The issue here is that this function can be + // called in response to a call to + // `IPlugFrame::resizeView()`. That function is always + // called from the UI thread, so we need some way to run + // code on the same thread that's currently waiting for a + // response to the message it sent. See the docstring of + // this function for more information on how this works. + return do_mutual_recursion_or_handle_in_main_context( + [&]() { + return object_instances[request.owner_instance_id] + .plug_view->onSize(&request.new_size); + }); + }, + [&](const YaPlugView::OnFocus& request) + -> YaPlugView::OnFocus::Response { + return main_context + .run_in_context([&]() { + return object_instances[request.owner_instance_id] + .plug_view->onFocus(request.state); + }) + .get(); + }, + [&](YaPlugView::SetFrame& request) + -> YaPlugView::SetFrame::Response { + // We'll create a proxy object for the `IPlugFrame` object and + // pass that to the `setFrame()` function. The lifetime of this + // object is tied to that of the actual `IPlugFrame` object + // we're passing this proxy to. + object_instances[request.owner_instance_id].plug_frame_proxy = + Steinberg::owned(new Vst3PlugFrameProxyImpl( + *this, std::move(request.plug_frame_args))); + + // TODO: Does this have to be run from the UI thread? Figure out + // if it does + return object_instances[request.owner_instance_id] + .plug_view->setFrame( + object_instances[request.owner_instance_id] + .plug_frame_proxy); + }, + [&](YaPlugView::CanResize& request) + -> YaPlugView::CanResize::Response { + return object_instances[request.owner_instance_id] + .plug_view->canResize(); + }, + [&](YaPlugView::CheckSizeConstraint& request) + -> YaPlugView::CheckSizeConstraint::Response { + return object_instances[request.owner_instance_id] + .plug_view->checkSizeConstraint(&request.rect); + }, + [&](YaPluginBase::Initialize& request) + -> YaPluginBase::Initialize::Response { + // We'll create a proxy object for the host context passed by + // the host and pass that to the initialize function. The + // lifetime of this object is tied to that of the actual plugin + // object we're proxying for. + // TODO: This needs changing if it turns out we need a + // `Vst3HostProxy` + object_instances[request.instance_id].host_context_proxy = + Steinberg::owned(new Vst3HostContextProxyImpl( + *this, std::move(request.host_context_args))); + + // XXX: Should `IPlugView::{initialize,terminate}` be run from + // the main UI thread? I can see how plugins would want to + // start timers from here. + return main_context + .run_in_context([&]() { + return object_instances[request.instance_id] + .plugin_base->initialize( + object_instances[request.instance_id] + .host_context_proxy); + }) + .get(); + }, + [&](const YaPluginBase::Terminate& request) + -> YaPluginBase::Terminate::Response { + return main_context + .run_in_context([&]() { + return object_instances[request.instance_id] + .plugin_base->terminate(); + }) + .get(); + }, + [&](const YaProgramListData::ProgramDataSupported& request) + -> YaProgramListData::ProgramDataSupported::Response { + return object_instances[request.instance_id] + .program_list_data->programDataSupported(request.list_id); + }, + [&](const YaProgramListData::GetProgramData& request) + -> YaProgramListData::GetProgramData::Response { + VectorStream data{}; + const tresult result = + object_instances[request.instance_id] + .program_list_data->getProgramData( + request.list_id, request.program_index, &data); + + return YaProgramListData::GetProgramDataResponse{ + .result = result, .data = std::move(data)}; + }, + [&](YaProgramListData::SetProgramData& request) + -> YaProgramListData::SetProgramData::Response { + return object_instances[request.instance_id] + .program_list_data->setProgramData( + request.list_id, request.program_index, &request.data); + }, + [&](const YaUnitData::UnitDataSupported& request) + -> YaUnitData::UnitDataSupported::Response { + return object_instances[request.instance_id] + .unit_data->unitDataSupported(request.unit_id); + }, + [&](const YaUnitData::GetUnitData& request) + -> YaUnitData::GetUnitData::Response { + VectorStream data{}; + const tresult result = + object_instances[request.instance_id] + .unit_data->getUnitData(request.unit_id, &data); + + return YaUnitData::GetUnitDataResponse{.result = result, + .data = std::move(data)}; + }, + [&](YaUnitData::SetUnitData& request) + -> YaUnitData::SetUnitData::Response { + return object_instances[request.instance_id] + .unit_data->setUnitData(request.unit_id, &request.data); + }, + [&](const YaPluginFactory::Construct&) + -> YaPluginFactory::Construct::Response { + return YaPluginFactory::ConstructArgs( + module->getFactory().get()); + }, + [&](YaPluginFactory::SetHostContext& request) + -> YaPluginFactory::SetHostContext::Response { + plugin_factory_host_context = + Steinberg::owned(new Vst3HostContextProxyImpl( + *this, std::move(request.host_context_args))); + + Steinberg::FUnknownPtr factory_3( + module->getFactory().get()); + assert(factory_3); + + return factory_3->setHostContext(plugin_factory_host_context); + }, + [&](const YaUnitInfo::GetUnitCount& request) + -> YaUnitInfo::GetUnitCount::Response { + return object_instances[request.instance_id] + .unit_info->getUnitCount(); + }, + [&](const YaUnitInfo::GetUnitInfo& request) + -> YaUnitInfo::GetUnitInfo::Response { + Steinberg::Vst::UnitInfo info; + const tresult result = + object_instances[request.instance_id] + .unit_info->getUnitInfo(request.unit_index, info); + + return YaUnitInfo::GetUnitInfoResponse{.result = result, + .info = std::move(info)}; + }, + [&](const YaUnitInfo::GetProgramListCount& request) + -> YaUnitInfo::GetProgramListCount::Response { + return object_instances[request.instance_id] + .unit_info->getProgramListCount(); + }, + [&](const YaUnitInfo::GetProgramListInfo& request) + -> YaUnitInfo::GetProgramListInfo::Response { + Steinberg::Vst::ProgramListInfo info; + const tresult result = object_instances[request.instance_id] + .unit_info->getProgramListInfo( + request.list_index, info); + + return YaUnitInfo::GetProgramListInfoResponse{ + .result = result, .info = std::move(info)}; + }, + [&](const YaUnitInfo::GetProgramName& request) + -> YaUnitInfo::GetProgramName::Response { + Steinberg::Vst::String128 name{0}; + const tresult result = + object_instances[request.instance_id] + .unit_info->getProgramName(request.list_id, + request.program_index, name); + + return YaUnitInfo::GetProgramNameResponse{ + .result = result, .name = tchar_pointer_to_u16string(name)}; + }, + [&](const YaUnitInfo::GetProgramInfo& request) + -> YaUnitInfo::GetProgramInfo::Response { + Steinberg::Vst::String128 attribute_value{0}; + const tresult result = + object_instances[request.instance_id] + .unit_info->getProgramInfo( + request.list_id, request.program_index, + request.attribute_id.c_str(), attribute_value); + + return YaUnitInfo::GetProgramInfoResponse{ + .result = result, + .attribute_value = + tchar_pointer_to_u16string(attribute_value)}; + }, + [&](const YaUnitInfo::HasProgramPitchNames& request) + -> YaUnitInfo::HasProgramPitchNames::Response { + return object_instances[request.instance_id] + .unit_info->hasProgramPitchNames(request.list_id, + request.program_index); + }, + [&](const YaUnitInfo::GetProgramPitchName& request) + -> YaUnitInfo::GetProgramPitchName::Response { + Steinberg::Vst::String128 name{0}; + const tresult result = + object_instances[request.instance_id] + .unit_info->getProgramPitchName( + request.list_id, request.program_index, + request.midi_pitch, name); + + return YaUnitInfo::GetProgramPitchNameResponse{ + .result = result, .name = tchar_pointer_to_u16string(name)}; + }, + [&](const YaUnitInfo::GetSelectedUnit& request) + -> YaUnitInfo::GetSelectedUnit::Response { + return object_instances[request.instance_id] + .unit_info->getSelectedUnit(); + }, + [&](const YaUnitInfo::SelectUnit& request) + -> YaUnitInfo::SelectUnit::Response { + return object_instances[request.instance_id] + .unit_info->selectUnit(request.unit_id); + }, + [&](const YaUnitInfo::GetUnitByBus& request) + -> YaUnitInfo::GetUnitByBus::Response { + Steinberg::Vst::UnitID unit_id; + const tresult result = + object_instances[request.instance_id] + .unit_info->getUnitByBus(request.type, request.dir, + request.bus_index, + request.channel, unit_id); + + return YaUnitInfo::GetUnitByBusResponse{.result = result, + .unit_id = unit_id}; + }, + [&](YaUnitInfo::SetUnitProgramData& request) + -> YaUnitInfo::SetUnitProgramData::Response { + return object_instances[request.instance_id] + .unit_info->setUnitProgramData(request.list_or_unit_id, + request.program_index, + &request.data); + }, + }); +} + +void Vst3Bridge::handle_x11_events() { + std::lock_guard lock(object_instances_mutex); + + for (const auto& [instance_id, object] : object_instances) { + if (object.editor) { + object.editor->handle_x11_events(); + } + } +} + +void Vst3Bridge::handle_win32_events() { + MSG msg; + + for (int i = 0; + i < max_win32_messages && PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE); + i++) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + +size_t Vst3Bridge::generate_instance_id() { + return current_instance_id.fetch_add(1); +} + +size_t Vst3Bridge::register_object_instance( + Steinberg::IPtr object) { + std::lock_guard lock(object_instances_mutex); + + const size_t instance_id = generate_instance_id(); + object_instances.emplace(instance_id, std::move(object)); + + // If the object supports `IComponent` or `IAudioProcessor`, + // then we'll set up a dedicated thread for function calls for + // those interfaces. + if (object_instances[instance_id].audio_processor || + object_instances[instance_id].component) { + std::promise socket_listening_latch; + + object_instances[instance_id] + .audio_processor_handler = Win32Thread([&, instance_id]() { + sockets.add_audio_processor_and_listen( + instance_id, socket_listening_latch, + overload{ + [&](YaAudioProcessor::SetBusArrangements& request) + -> YaAudioProcessor::SetBusArrangements::Response { + return object_instances[request.instance_id] + .audio_processor->setBusArrangements( + request.inputs.data(), request.num_ins, + request.outputs.data(), request.num_outs); + }, + [&](YaAudioProcessor::GetBusArrangement& request) + -> YaAudioProcessor::GetBusArrangement::Response { + const tresult result = + object_instances[request.instance_id] + .audio_processor->getBusArrangement( + request.dir, request.index, request.arr); + + return YaAudioProcessor::GetBusArrangementResponse{ + .result = result, .updated_arr = request.arr}; + }, + [&](const YaAudioProcessor::CanProcessSampleSize& request) + -> YaAudioProcessor::CanProcessSampleSize::Response { + return object_instances[request.instance_id] + .audio_processor->canProcessSampleSize( + request.symbolic_sample_size); + }, + [&](const YaAudioProcessor::GetLatencySamples& request) + -> YaAudioProcessor::GetLatencySamples::Response { + return object_instances[request.instance_id] + .audio_processor->getLatencySamples(); + }, + [&](YaAudioProcessor::SetupProcessing& request) + -> YaAudioProcessor::SetupProcessing::Response { + return object_instances[request.instance_id] + .audio_processor->setupProcessing(request.setup); + }, + [&](const YaAudioProcessor::SetProcessing& request) + -> YaAudioProcessor::SetProcessing::Response { + return object_instances[request.instance_id] + .audio_processor->setProcessing(request.state); + }, + [&](YaAudioProcessor::Process& request) + -> YaAudioProcessor::Process::Response { + const tresult result = + object_instances[request.instance_id] + .audio_processor->process(request.data.get()); + + return YaAudioProcessor::ProcessResponse{ + .result = result, + .output_data = + request.data.move_outputs_to_response()}; + }, + [&](const YaAudioProcessor::GetTailSamples& request) + -> YaAudioProcessor::GetTailSamples::Response { + return object_instances[request.instance_id] + .audio_processor->getTailSamples(); + }, + [&](const YaComponent::GetControllerClassId& request) + -> YaComponent::GetControllerClassId::Response { + Steinberg::TUID cid; + const tresult result = + object_instances[request.instance_id] + .component->getControllerClassId(cid); + + return YaComponent::GetControllerClassIdResponse{ + .result = result, .editor_cid = std::to_array(cid)}; + }, + [&](const YaComponent::SetIoMode& request) + -> YaComponent::SetIoMode::Response { + return object_instances[request.instance_id] + .component->setIoMode(request.mode); + }, + [&](const YaComponent::GetBusCount& request) + -> YaComponent::GetBusCount::Response { + return object_instances[request.instance_id] + .component->getBusCount(request.type, request.dir); + }, + [&](YaComponent::GetBusInfo& request) + -> YaComponent::GetBusInfo::Response { + const tresult result = + object_instances[request.instance_id] + .component->getBusInfo( + request.type, request.dir, request.index, + request.bus); + + return YaComponent::GetBusInfoResponse{ + .result = result, .updated_bus = request.bus}; + }, + [&](YaComponent::GetRoutingInfo& request) + -> YaComponent::GetRoutingInfo::Response { + const tresult result = + object_instances[request.instance_id] + .component->getRoutingInfo(request.in_info, + request.out_info); + + return YaComponent::GetRoutingInfoResponse{ + .result = result, + .updated_in_info = request.in_info, + .updated_out_info = request.out_info}; + }, + [&](const YaComponent::ActivateBus& request) + -> YaComponent::ActivateBus::Response { + return object_instances[request.instance_id] + .component->activateBus(request.type, request.dir, + request.index, + request.state); + }, + [&](const YaComponent::SetActive& request) + -> YaComponent::SetActive::Response { + return object_instances[request.instance_id] + .component->setActive(request.state); + }, + }); + }); + + // Wait for the new socket to be listening on before + // continuing. Otherwise the native plugin may try to + // connect to it before our thread is up and running. + socket_listening_latch.get_future().wait(); + } + + return instance_id; +} + +void Vst3Bridge::unregister_object_instance(size_t instance_id) { + // Tear the dedicated audio processing socket down again if we + // created one while handling `Vst3PluginProxy::Construct` + if (object_instances[instance_id].audio_processor || + object_instances[instance_id].component) { + sockets.remove_audio_processor(instance_id); + } + + // Remove the instance from within the main IO context so + // removing it doesn't interfere with the Win32 message loop + // XXX: I don't think we have to wait for the object to be + // deleted most of the time, but I can imagine a situation + // where the plugin does a host callback triggered by a + // Win32 timer in between where the above closure is being + // executed and when the actual host application context on + // the plugin side gets deallocated. + main_context + .run_in_context([&, instance_id]() { + std::lock_guard lock(object_instances_mutex); + object_instances.erase(instance_id); + }) + .wait(); +} diff --git a/src/wine-host/bridges/vst3.h b/src/wine-host/bridges/vst3.h new file mode 100644 index 00000000..20ab77a0 --- /dev/null +++ b/src/wine-host/bridges/vst3.h @@ -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 . + +#pragma once + +#include +#include + +#include + +#include "../../common/communication/vst3.h" +#include "../../common/configuration.h" +#include "../editor.h" +#include "common.h" + +/** + * A holder for plugin object instance created from the factory. This stores all + * relevant interface smart pointers to that object so we can handle control + * messages sent by the plugin without having to do these expensive casts all + * the time. This also stores any additional context data, such as the + * `IHostApplication` instance passed to the plugin during + * `IPluginBase::initialize()`. + */ +struct InstanceInterfaces { + InstanceInterfaces(); + + InstanceInterfaces(Steinberg::IPtr object); + + /** + * A dedicated thread for handling incoming `IAudioProcessor` and + * `IComponent` calls. Will be instantiated if `object` supports either of + * those interfaces. + */ + Win32Thread audio_processor_handler; + + /** + * If the host passes a host context object during + * `IPluginBase::initialize()`, we'll store a proxy object here and then + * pass it to `plugin_base->initialize()`. Will be initialized with a null + * pointer until used. + */ + Steinberg::IPtr host_context_proxy; + + /** + * If the host connects two objects indirectly using a connection proxy (as + * allowed by the VST3 specification), then we also can't connect the + * objects directly on the Wine side. In that case we'll have to create this + * proxy object, pass it to the plugin, and if the plugin then calls + * `IConnectionPoint::notify()` on it we'll pass that call through to the + * `IConnectionPoint` instance passed to us by the host (which will then in + * turn call `IConnectionPoint::notify()` on our plugin proxy object). + * Proxies for days. + */ + Steinberg::IPtr connection_point_proxy; + + /** + * After a call to `IEditController::setComponentHandler()`, we'll create a + * proxy of that component handler just like we did for the plugin object. + * When the plugin calls a function on this object, we make a callback to + * the original object provided by the host. Will be initialized with a null + * pointer until used. + */ + Steinberg::IPtr component_handler_proxy; + + /** + * If the host passes an `IPlugFrame` object during `IPlugView::setFrame()`, + * then we'll store a proxy object here and then pass it to + * `plug_view->setFrame()`. Will be initialized with a null pointer until + * used. When we destroy `plug_view` while handling + * `Vst3PlugViewProxy::Destruct`, we'll also destroy (our pointer of) this + * proxy object. + */ + Steinberg::IPtr plug_frame_proxy; + + /** + * The base object we cast from. + */ + Steinberg::IPtr object; + + /** + * The `IPlugView` object the plugin returned from a call to + * `IEditController::createView()`. + * + * XXX: Technically VST3 supports multiple named views, so we could have + * multiple different view for a single plugin. This is not used within + * the SDK, so a single pointer should be fine for now. + */ + Steinberg::IPtr plug_view; + + /** + * This instance's editor, if it has an open editor. Embedding here works + * exactly the same as how it works for VST2 plugins. + */ + std::optional editor; + + // All smart pointers below are created from `component`. They will be null + // pointers if `component` did not implement the interface. + + Steinberg::FUnknownPtr audio_processor; + Steinberg::FUnknownPtr component; + Steinberg::FUnknownPtr connection_point; + Steinberg::FUnknownPtr edit_controller; + Steinberg::FUnknownPtr edit_controller_2; + Steinberg::FUnknownPtr plugin_base; + Steinberg::FUnknownPtr unit_data; + Steinberg::FUnknownPtr program_list_data; + Steinberg::FUnknownPtr unit_info; +}; + +/** + * This hosts a Windows VST3 plugin, forwards messages sent by the Linux VST + * plugin and provides host callback function for the plugin to talk back. + */ +class Vst3Bridge : public HostBridge { + public: + /** + * Initializes the Windows VST3 plugin and set up communication with the + * native Linux VST plugin. + * + * @param main_context The main IO context for this application. Most events + * will be dispatched to this context, and the event handling loop should + * also be run from this context. + * @param plugin_dll_path A (Unix style) path to the VST plugin .dll file to + * load. + * @param endpoint_base_dir The base directory used for the socket + * endpoints. See `Sockets` for more information. + * + * @note The object has to be constructed from the same thread that calls + * `main_context.run()`. + * + * @throw std::runtime_error Thrown when the VST plugin could not be loaded, + * or if communication could not be set up. + */ + Vst3Bridge(MainContext& main_context, + std::string plugin_dll_path, + std::string endpoint_base_dir); + + /** + * Here we'll listen for and handle incoming control messages until the + * sockets get closed. + */ + void run() override; + + void handle_x11_events() override; + void handle_win32_events() override; + + /** + * Send a callback message to the host return the response. This is a + * shorthand for `sockets.vst_host_callback.send_message` for use in VST3 + * interface implementations. + */ + template + typename T::Response send_message(const T& object) { + return sockets.vst_host_callback.send_message(object, std::nullopt); + } + + /** + * Spawn a new thread and call `send_message()` from there, and then handle + * functions passed by calls to + * `do_mutual_recursion_or_handle_in_main_context()` on this thread until + * the original message we're trying to send has succeeded. This is a very + * specific solution to a very specific problem. When a plugin wants to + * resize itself, it will call `IPlugFrame::resizeView()` from within the + * WIn32 message loop. The host will then call `IPlugView::onSize()` on the + * plugin's `IPlugView` to actually resize the plugin. The issue is that + * that call to `IPlugView::onSize()` has to be handled from the UI thread, + * but in this sequence that thread is being blocked by a call to + * `IPlugFrame::resizeView()`. + * + * The hacky solution here is to send the message from another thread, and + * to then allow this thread to execute other functions submitted to an IO + * context. + */ + template + typename T::Response send_mutually_recursive_message(const T& object) { + using TResponse = typename T::Response; + + // This IO context will accept incoming calls from + // `do_mutual_recursion_or_handle_in_main_context()` until we receive a + // response + { + std::unique_lock lock(mutual_recursion_context_mutex); + + // In case some other thread is already calling + // `send_mutually_recursive_message()` (which should never be the + // case since this should only be called from the UI thread), we'll + // wait for that function to finish + if (mutual_recursion_context) { + mutual_recursion_context_cv.wait(lock, [&]() { + return !mutual_recursion_context.has_value(); + }); + } + + mutual_recursion_context.emplace(); + } + + // We will call the function from another thread so we can handle calls + // to from this thread + std::promise response_promise{}; + Win32Thread sending_thread([&]() { + const TResponse response = send_message(object); + + // Stop accepting additional work to be run from the calling thread + // once we receive a response + { + std::lock_guard lock(mutual_recursion_context_mutex); + mutual_recursion_context->stop(); + mutual_recursion_context.reset(); + } + mutual_recursion_context_cv.notify_one(); + + response_promise.set_value(response); + }); + + // Accept work from the other thread until we receive a response, at + // which point the context will be stopped + auto work_guard = + boost::asio::make_work_guard(*mutual_recursion_context); + mutual_recursion_context->run(); + + return response_promise.get_future().get(); + } + + /** + * Crazy functions ask for crazy naming. This is the other part of + * `send_mutually_recursive_message()`. If another thread is currently + * calling that function (from the UI thread), then we'll execute `f` from + * the UI thread using the IO context started in the above function. + * Otherwise `f` will be run on the UI thread through `main_context` as + * usual. + * + * @see Vst3Bridge::send_mutually_recursive_message + */ + template + T do_mutual_recursion_or_handle_in_main_context(F f) { + std::packaged_task do_call(f); + std::future do_call_response = do_call.get_future(); + + // If the above function is currently being called from some thread, + // then we'll submit the task to the IO context created there so it can + // be handled on that same thread. Otherwise we'll just submit it to the + // main IO context. Neither of these two functions block until `do_call` + // finish executing. + { + std::lock_guard lock(mutual_recursion_context_mutex); + if (mutual_recursion_context) { + boost::asio::dispatch(*mutual_recursion_context, + std::move(do_call)); + } else { + main_context.schedule_task(std::move(do_call)); + } + } + + return do_call_response.get(); + } + + private: + Logger generic_logger; + + public: + /** + * A logger instance we'll use to log about failed + * `FUnknown::queryInterface` calls, so they can be hidden on verbosity + * level 0. + * + * This only has to be used instead of directly writing to `std::cerr` when + * the message should be hidden on lower verbosity levels. + */ + Vst3Logger logger; + + private: + /** + * Generate a nique instance identifier using an atomic fetch-and-add. This + * is used to be able to refer to specific instances created for + * `IPluginFactory::createInstance()`. + */ + size_t generate_instance_id(); + + /** + * Assign a unique identifier to an object and add it to `object_instances`. + * This will also set up listeners for `IAudioProcessor` and `IComponent` + * function calls. + */ + size_t register_object_instance( + Steinberg::IPtr object); + + /** + * Remove an object from `object_instances`. Will also tear down the + * `IAudioProcessor`/`IComponent` socket if it had one. + */ + void unregister_object_instance(size_t instance_id); + + /** + * The IO context used for event handling so that all events and window + * message handling can be performed from a single thread, even when hosting + * multiple plugins. + */ + MainContext& main_context; + + /** + * The configuration for this instance of yabridge based on the `.so` file + * that got loaded by the host. This configuration gets loaded on the plugin + * side, and then sent over to the Wine host as part of the startup process. + */ + Configuration config; + + std::shared_ptr module; + + /** + * All sockets used for communicating with this specific plugin. + * + * NOTE: This is defined **after** the threads on purpose. This way the + * sockets will be closed first, and we can then safely wait for the + * threads to exit. + */ + Vst3Sockets sockets; + + /** + * Used to assign unique identifier to instances created for + * `IPluginFactory::createInstance()`. + * + * @related enerate_instance_id + */ + std::atomic_size_t current_instance_id; + + /** + * The host context proxy object if we got passed a host context during a + * call to `IPluginFactory3::setHostContext()` by the host. + */ + Steinberg::IPtr plugin_factory_host_context; + + /** + * These are all the objects we have created through the Windows VST3 + * plugins' plugin factory. The keys in all of these maps are the unique + * identifiers we generated for them so we can identify specific instances. + * During the proxy object's destructor (on the plugin side), we'll get a + * request to remove the corresponding plugin object from this map. This + * will cause all pointers to it to get dropped and the object to be cleaned + * up. + */ + std::map object_instances; + std::mutex object_instances_mutex; + + /** + * The IO context used in `send_mutually_recursive_message()` to be able to + * execute functions from that same calling thread while we're waiting for a + * response. See the docstring there for more information. When this doesn't + * contain an IO context, this function is not being called and + * `do_mutual_recursion_or_handle_in_main_context()` should post the task + * directly to the main IO context. + */ + std::optional mutual_recursion_context; + std::mutex mutual_recursion_context_mutex; + /** + * Used to make sure only a single call to + * `send_mutually_recursive_message()` at a time can be processed (this + * should never happen, but better safe tha nsorry). + */ + std::condition_variable mutual_recursion_context_cv; +}; diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index da5234fe..af22697a 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -18,14 +18,11 @@ #include -// The Win32 API requires you to hardcode identifiers for tiemrs -constexpr size_t idle_timer_id = 1337; - /** - * The most significant bit in an event's response type is used to indicate - * whether the event source. + * The most significant bit in an X11 event's response type is used to indicate + * the event source. */ -constexpr uint16_t event_type_mask = ((1 << 7) - 1); +constexpr uint16_t event_type_mask = 0b0111'1111; /** * The name of the X11 property on the root window used to denote the active @@ -33,6 +30,21 @@ constexpr uint16_t event_type_mask = ((1 << 7) - 1); */ constexpr char active_window_property_name[] = "_NET_ACTIVE_WINDOW"; +/** + * Client message name for XEmbed messages. See + * https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html. + */ +constexpr char xembed_message_name[] = "_XEMBED"; + +// Constants from the XEmbed spec +constexpr uint32_t xembed_protocol_version = 0; + +constexpr uint32_t xembed_embedded_notify_msg = 0; +constexpr uint32_t xembed_window_activate_msg = 1; +constexpr uint32_t xembed_focus_in_msg = 4; + +constexpr uint32_t xembed_focus_first = 1; + /** * Find the topmost window (i.e. the window before the root window in the window * tree) starting from a certain window. @@ -84,9 +96,9 @@ WindowClass::~WindowClass() { Editor::Editor(const Configuration& config, const std::string& window_class_name, - const size_t parent_window_handle, - AEffect* effect) - : x11_connection(xcb_connect(nullptr, nullptr), xcb_disconnect), + const size_t parent_window_handle) + : use_xembed(config.editor_xembed), + x11_connection(xcb_connect(nullptr, nullptr), xcb_disconnect), client_area(get_maximum_screen_dimensions(*x11_connection)), window_class(window_class_name), // Create a window without any decoratiosn for easy embedding. The @@ -110,17 +122,15 @@ Editor::Editor(const Configuration& config, // If `config.editor_double_embed` is set, then we'll also create a child // window in `win32_child_handle`. If we do this before calling // `ShowWindow()` on `win32_handle` we'll run into X11 errors. - idle_timer(win32_handle.get(), idle_timer_id, 100), + win32_child_handle(std::nullopt), parent_window(parent_window_handle), wine_window(get_x11_handle(win32_handle.get())), - topmost_window(find_topmost_window(*x11_connection, parent_window)), - // Needed to send update messages on a timer - plugin(effect) { + topmost_window(find_topmost_window(*x11_connection, parent_window)) { xcb_generic_error_t* error; // Used for input focus grabbing to only grab focus when the window is // active. - const xcb_intern_atom_cookie_t atom_cookie = xcb_intern_atom( + xcb_intern_atom_cookie_t atom_cookie = xcb_intern_atom( x11_connection.get(), true, strlen(active_window_property_name), active_window_property_name); xcb_intern_atom_reply_t* atom_reply = @@ -141,59 +151,87 @@ Editor::Editor(const Configuration& config, << std::endl; } - // 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 because this is sometimes needed for plugin groups. We also listen - // for EnterNotify and LeaveNotify events on the Wine window so we can grab - // and release input focus as necessary. - const uint32_t topmost_event_mask = XCB_EVENT_MASK_STRUCTURE_NOTIFY; + // When using XEmbed we'll need the atoms for the corresponding properties + atom_cookie = + xcb_intern_atom(x11_connection.get(), true, strlen(xembed_message_name), + xembed_message_name); + atom_reply = + xcb_intern_atom_reply(x11_connection.get(), atom_cookie, &error); + assert(!error); + xcb_xembed_message = atom_reply->atom; + free(atom_reply); + + // When 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 because + // this is sometimes needed for plugin groups. We also listen for + // EnterNotify and LeaveNotify events on the Wine window so we can grab and + // release input focus as necessary. + // If we do enable XEmbed support, we'll also listen for visibility changes + // and trigger the embedding when the window becomes visible + const uint32_t topmost_event_mask = + XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_VISIBILITY_CHANGE; 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_EVENT_MASK_LEAVE_WINDOW; + const uint32_t parent_event_mask = + XCB_EVENT_MASK_FOCUS_CHANGE | XCB_EVENT_MASK_ENTER_WINDOW | + XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_VISIBILITY_CHANGE; 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 - // child window ourselves. This is a hack to work around the issue's - // described in `Editor`'s docstring'. - xcb_reparent_window(x11_connection.get(), wine_window, parent_window, 0, 0); - xcb_flush(x11_connection.get()); + if (use_xembed) { + // This call alone doesn't do anything. We need to call this function a + // second time on visibility change because Wine's XEmbed implementation + // does not work properly (which is why we remvoed XEmbed support in the + // first place). + do_xembed(); + } else { + // 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 child window ourselves. This is a hack to work around the issue's + // described in `Editor`'s docstring'. + xcb_reparent_window(x11_connection.get(), wine_window, parent_window, 0, + 0); + xcb_flush(x11_connection.get()); - ShowWindow(win32_handle.get(), SW_SHOWNORMAL); - if (config.editor_double_embed) { - // FIXME: This emits `-Wignored-attributes` as of Wine 5.22 + // If we're using the double embedding option, then the child window + // should only be created after the parent window is visible + ShowWindow(win32_handle.get(), SW_SHOWNORMAL); + if (config.editor_double_embed) { + // FIXME: This emits `-Wignored-attributes` as of Wine 5.22 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wignored-attributes" - // As explained above, we can't do this directly in the initializer list - win32_child_handle = std::unique_ptr, - decltype(&DestroyWindow)>( - CreateWindowEx( - WS_EX_TOOLWINDOW, reinterpret_cast(window_class.atom), - "yabridge plugin child", WS_CHILD, CW_USEDEFAULT, CW_USEDEFAULT, - client_area.width, client_area.height, win32_handle.get(), - nullptr, GetModuleHandle(nullptr), this), - DestroyWindow); + // As explained above, we can't do this directly in the initializer + // list + win32_child_handle = std::unique_ptr, + decltype(&DestroyWindow)>( + CreateWindowEx(WS_EX_TOOLWINDOW, + reinterpret_cast(window_class.atom), + "yabridge plugin child", WS_CHILD, CW_USEDEFAULT, + CW_USEDEFAULT, client_area.width, + client_area.height, win32_handle.get(), nullptr, + GetModuleHandle(nullptr), this), + DestroyWindow); #pragma GCC diagnostic pop - ShowWindow(win32_child_handle->get(), SW_SHOWNORMAL); - } + ShowWindow(win32_child_handle->get(), SW_SHOWNORMAL); + } - // HACK: I can't seem to figure why the initial reparent would fail on this - // particular i3 config in a way that I'm unable to reproduce, but if - // it doesn't work the first time, just keep trying! - // - // https://github.com/robot-vdh/yabridge/issues/40 - xcb_reparent_window(x11_connection.get(), wine_window, parent_window, 0, 0); - xcb_flush(x11_connection.get()); + // HACK: I can't seem to figure why the initial reparent would fail on + // this particular i3 config in a way that I'm unable to + // reproduce, but if it doesn't work the first time, just keep + // trying! + // + // https://github.com/robot-vdh/yabridge/issues/40 + xcb_reparent_window(x11_connection.get(), wine_window, parent_window, 0, + 0); + xcb_flush(x11_connection.get()); + } } Editor::~Editor() { @@ -218,48 +256,15 @@ Editor::~Editor() { } HWND Editor::get_win32_handle() const { - if (win32_child_handle) { + // FIXME: The double embed and XEmbed options don't work together right now + if (win32_child_handle && !use_xembed) { return win32_child_handle->get(); } else { return win32_handle.get(); } } -void Editor::send_idle_event() { - plugin->dispatcher(plugin, effEditIdle, 0, 0, nullptr, 0); -} - -void Editor::handle_win32_events() const { - MSG msg; - - // The null value for the second argument is needed to handle interaction - // with child GUI components. So far limiting this to `max_win32_messages` - // messages has only been needed for Waves plugins as they otherwise cause - // an infinite message loop. - for (int i = 0; - i < max_win32_messages && PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE); - i++) { - // This timer would periodically send `effEditIdle` events so the editor - // remains responsive even during blocking GUI operations such as open - // dropdowns or message boxes. This is only needed when the GUI is - // actually blocked and it will be dispatched by the messaging loop of - // the blocking GUI component. Since we're not touching the - // `effEditIdle` event sent by the host we can always filter this timer - // event out in this event loop. - if (msg.message == WM_TIMER && msg.wParam == idle_timer_id && - msg.hwnd == win32_handle.get()) { - continue; - } - - TranslateMessage(&msg); - DispatchMessage(&msg); - } -} - void Editor::handle_x11_events() const { - // 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. xcb_generic_event_t* generic_event; while ((generic_event = xcb_poll_for_event(x11_connection.get())) != nullptr) { @@ -273,7 +278,17 @@ void Editor::handle_x11_events() const { // check is sometimes necessary for using multiple editor windows // within a single plugin group. case XCB_CONFIGURE_NOTIFY: - fix_local_coordinates(); + if (!use_xembed) { + fix_local_coordinates(); + } + break; + // Start the XEmbed procedure when the window becomes visible, since + // most hosts will only show the window after the plugin has + // embedded itself into it. + case XCB_VISIBILITY_NOTIFY: + if (use_xembed) { + do_xembed(); + } break; // We want to grab keyboard input focus when the user hovers over // our embedded Wine window AND that window is a child of the @@ -286,7 +301,9 @@ void Editor::handle_x11_events() const { // `EnterNotify'. case XCB_ENTER_NOTIFY: case XCB_FOCUS_IN: - fix_local_coordinates(); + if (!use_xembed) { + fix_local_coordinates(); + } // In case the WM somehow does not support `_NET_ACTIVE_WINDOW`, // a more naive focus grabbing method implemented in the @@ -327,7 +344,11 @@ void Editor::handle_x11_events() const { } void Editor::fix_local_coordinates() const { - // We're purposely not using XEmbed. This has the consequence that wine + if (use_xembed) { + return; + } + + // We're purposely not using XEmbed here. 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 @@ -383,6 +404,10 @@ void Editor::set_input_focus(bool grab) const { // back to that window when the user moves their mouse outside of the Wine // window while the host's window is still active (that's an important // detail, since plugins may have dialogs). + // XXX: In theory we wouldn't have to do this for VST3 because + // `IPlugView::onKey{Down,Up}` should handle all keyboard events. But + // in practice a lot of hosts don't use that, so we still need to grab + // focus ourselves. xcb_set_input_focus(x11_connection.get(), XCB_INPUT_FOCUS_PARENT, grab ? parent_window : topmost_window, XCB_CURRENT_TIME); @@ -449,6 +474,51 @@ bool Editor::supports_ewmh_active_window() const { return active_window_property_exists; } +void Editor::send_xembed_message(const xcb_window_t& window, + const uint32_t message, + const uint32_t detail, + const uint32_t data1, + const uint32_t data2) const { + xcb_client_message_event_t event; + event.response_type = XCB_CLIENT_MESSAGE; + event.type = xcb_xembed_message; + event.window = window; + event.format = 32; + event.data.data32[0] = XCB_CURRENT_TIME; + event.data.data32[1] = message; + event.data.data32[2] = detail; + event.data.data32[3] = data1; + event.data.data32[4] = data2; + + xcb_send_event(x11_connection.get(), false, window, XCB_EVENT_MASK_NO_EVENT, + reinterpret_cast(&event)); +} + +void Editor::do_xembed() const { + if (!use_xembed) { + return; + } + + // If we're embedding using XEmbed, then we'll have to go through the whole + // XEmbed dance here. See the spec for more information on how this works: + // https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html#lifecycle + xcb_reparent_window(x11_connection.get(), wine_window, parent_window, 0, 0); + xcb_flush(x11_connection.get()); + + // Let the Wine window know it's being embedded into the parent window + send_xembed_message(wine_window, xembed_embedded_notify_msg, 0, + parent_window, xembed_protocol_version); + send_xembed_message(wine_window, xembed_focus_in_msg, xembed_focus_first, 0, + 0); + send_xembed_message(wine_window, xembed_window_activate_msg, 0, 0, 0); + xcb_flush(x11_connection.get()); + + xcb_map_window(x11_connection.get(), wine_window); + xcb_flush(x11_connection.get()); + + ShowWindow(win32_handle.get(), SW_SHOWNORMAL); +} + LRESULT CALLBACK window_proc(HWND handle, UINT message, WPARAM wParam, @@ -470,19 +540,18 @@ LRESULT CALLBACK window_proc(HWND handle, SetWindowLongPtr(handle, GWLP_USERDATA, reinterpret_cast(editor)); } break; - case WM_TIMER: { + // Setting `SWP_NOCOPYBITS` somewhat reduces flickering on + // `fix_local_coordinates()` calls with plugins that don't do double + // buffering since it speeds up the redrawing process. + case WM_WINDOWPOSCHANGING: { auto editor = reinterpret_cast( GetWindowLongPtr(handle, GWLP_USERDATA)); - if (!editor || wParam != idle_timer_id) { + if (!editor || editor->use_xembed) { break; } - // We'll send idle messages on a timer. This way the plugin will get - // keep periodically updating its editor either when the host sends - // `effEditIdle` themself, or periodically when the GUI is being - // blocked by a dropdown or a message box. - editor->send_idle_event(); - return 0; + WINDOWPOS* info = reinterpret_cast(lParam); + info->flags |= SWP_NOCOPYBITS | SWP_DEFERERASE; } break; // In case the WM does not support the EWMH active window property, // we'll fall back to grabbing focus when the user clicks on the window @@ -610,6 +679,7 @@ ATOM register_window_class(std::string window_class_name) { window_class.lpfnWndProc = window_proc; window_class.hInstance = GetModuleHandle(nullptr); window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.hbrBackground = CreateSolidBrush(RGB(32, 32, 32)); window_class.lpszClassName = window_class_name.c_str(); return RegisterClassEx(&window_class); diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index c5989fb5..0ffce84e 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -14,24 +14,24 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +#pragma once + +#include +#include +#include + +#ifndef NOMINMAX +#define NOMINMAX +#define WINE_NOWINSOCK +#endif +#include + // Use the native version of xcb #pragma push_macro("_WIN32") #undef _WIN32 #include #pragma pop_macro("_WIN32") -#define NOMINMAX -#define NOSERVICE -#define NOMCX -#define NOIMM -#define WIN32_LEAN_AND_MEAN -#include -#include - -#include -#include -#include - #include "../common/configuration.h" #include "utils.h" @@ -84,7 +84,10 @@ class WindowClass { * window without using XEmbed. If anyone knows how to work around these two * issues, please let me know and I'll switch to using XEmbed again. * - * This workaround was inspired by LinVST. + * This workaround was inspired by LinVst. + * + * As of yabridge 3.0 XEmbed is back as an option, but it's disabled by default + * because of the issues mentioned above. */ class Editor { public: @@ -98,15 +101,12 @@ class Editor { * windows. * @param parent_window_handle The X11 window handle passed by the VST host * for the editor to embed itself into. - * @param effect The plugin this window is being created for. Used to send - * `effEditIdle` messages on a timer. * * @see win32_handle */ Editor(const Configuration& config, const std::string& window_class_name, - const size_t parent_window_handle, - AEffect* effect); + const size_t parent_window_handle); ~Editor(); @@ -128,20 +128,6 @@ class Editor { */ bool supports_ewmh_active_window() const; - /** - * Send a single `effEditIdle` event to the plugin to allow it to update its - * GUI state. This is called periodically from a timer while the GUI is - * being blocked, and also called explicitly by the host on a timer. - */ - void send_idle_event(); - - /** - * 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_win32_events() const; - /** * Handle X11 events sent to the window our editor is embedded in. */ @@ -165,6 +151,12 @@ class Editor { */ void set_input_focus(bool grab) const; + /** + * Whether to use XEmbed instead of yabridge's normal window embedded. Wine + * with XEmbed tends to cause rendering issues, so it's disabled by default. + */ + const bool use_xembed; + private: /** * Returns `true` if the currently active window (as per @@ -175,6 +167,24 @@ class Editor { */ bool is_wine_window_active() const; + /** + * Send an XEmbed message to a window. This does not include a flush. See + * the spec for more information: + * + * https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html#lifecycle + */ + void send_xembed_message(const xcb_window_t& window, + const uint32_t message, + const uint32_t detail, + const uint32_t data1, + const uint32_t data2) const; + + /** + * Start the XEmbed procedure when `use_xembed` is enabled. This should be + * rerun whenever visibility changes. + */ + void do_xembed() const; + /** * A pointer to the currently active window. Will be a null pointer if no * window is active. @@ -222,15 +232,6 @@ class Editor { #pragma GCC diagnostic pop - /** - * 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 the - * host will call `effEditIdle()` explicitely when the plugin is not busy. - */ - Win32Timer idle_timer; - /** * The window handle of the editor window created by the DAW. */ @@ -249,11 +250,6 @@ class Editor { */ const xcb_window_t topmost_window; - /** - *Needed to handle idle updates through a timer - */ - AEffect* plugin; - /** * The atom corresponding to `_NET_ACTIVE_WINDOW`. */ @@ -264,4 +260,9 @@ class Editor { * `supports_ewmh_active_window()`. */ mutable std::optional supports_ewmh_active_window_cache; + + /** + * The atom corresponding to `_XEMBED`. + */ + xcb_atom_t xcb_xembed_message; }; diff --git a/src/wine-host/group-host.cpp b/src/wine-host/group-host.cpp index d276ea6a..75585ba7 100644 --- a/src/wine-host/group-host.cpp +++ b/src/wine-host/group-host.cpp @@ -30,11 +30,11 @@ * 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. + * allowing the `libyabridge-{vst2,vst3}.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. */ int __attribute__((visibility("default"))) #ifdef WINE_USE_CDECL diff --git a/src/wine-host/individual-host.cpp b/src/wine-host/individual-host.cpp index 0693c171..d2279691 100644 --- a/src/wine-host/individual-host.cpp +++ b/src/wine-host/individual-host.cpp @@ -23,11 +23,14 @@ #include "../common/utils.h" #include "bridges/vst2.h" +#ifdef WITH_VST3 +#include "bridges/vst3.h" +#endif /** - * This is the default VST host application. It will load the specified VST2 - * plugin, and then connect back to the `libyabridge.so` instance that spawned - * this over the socket. + * This is the default plugin host application. It will load the specified + * plugin plugin, and then connect back to the `libyabridge-{vst2,vst3}.so` + * instance that spawned this over the socket. */ int __attribute__((visibility("default"))) #ifdef WINE_USE_CDECL @@ -36,29 +39,36 @@ __cdecl main(int argc, char* argv[]) { set_realtime_priority(); - // We pass the name of the VST plugin .dll file to load and the base - // directory for the Unix domain socket endpoints to connect to as the first - // two arguments of this process in plugin/bridge.cpp. - if (argc < 3) { - std::cerr << "Usage: " + // We pass plugin format, the name of the VST2 plugin .dll file or VST3 + // bundle to load, and the base directory for the Unix domain socket + // endpoints to connect to as the first two arguments of this process in + // `src/plugin/host-process.cpp` + if (argc < 4) { + std::cerr + << "Usage: " #ifdef __i386__ - << yabridge_individual_host_name_32bit + << yabridge_individual_host_name_32bit #else - << yabridge_individual_host_name + << yabridge_individual_host_name #endif - << " " << std::endl; + << " " + << std::endl; return 1; } - const std::string plugin_dll_path(argv[1]); - const std::string socket_endpoint_path(argv[2]); + const std::string plugin_type_str(argv[1]); + const PluginType plugin_type = plugin_type_from_string(plugin_type_str); + const std::string plugin_location(argv[2]); + const std::string socket_endpoint_path(argv[3]); std::cout << "Initializing yabridge host version " << yabridge_git_version #ifdef __i386__ << " (32-bit compatibility mode)" #endif << std::endl; + std::cout << "Preparing to load " << plugin_type_to_string(plugin_type) + << " plugin at '" << plugin_location << "'" << std::endl; // As explained in `Vst2Bridge`, the plugin has to be initialized in the // same thread as the one that calls `io_context.run()`. This setup is @@ -66,24 +76,43 @@ __cdecl // don't need to differentiate between individually hosted plugins and // plugin groups when it comes to event handling. MainContext main_context{}; - std::unique_ptr bridge; + std::unique_ptr bridge; try { - bridge = std::make_unique(main_context, plugin_dll_path, - socket_endpoint_path); + switch (plugin_type) { + case PluginType::vst2: + bridge = std::make_unique( + main_context, plugin_location, socket_endpoint_path); + break; + case PluginType::vst3: +#ifdef WITH_VST3 + bridge = std::make_unique( + main_context, plugin_location, socket_endpoint_path); +#else + std::cerr << "This version of yabridge has not been compiled " + "with VST3 support" + << std::endl; + return 1; +#endif + break; + case PluginType::unknown: + std::cerr << "Unknown plugin type '" << plugin_type_str << "'" + << std::endl; + return 1; + break; + }; } catch (const std::runtime_error& error) { - std::cerr << "Error while initializing Wine VST host:" << std::endl; + std::cerr << "Error while initializing the Wine plugin host:" + << std::endl; std::cerr << error.what() << std::endl; return 1; } - std::cout << "Finished initializing '" << plugin_dll_path << "'" - << std::endl; - - // We'll listen for `dispatcher()` calls on a different thread, but the - // actual events will still be executed within the IO context - Win32Thread dispatch_handler([&]() { - bridge->handle_dispatch(); + // Let the plugin receive and handle its events on its own thread. Some + // potentially unsafe events that should always be run from the UI thread + // will be posted to `main_context`. + Win32Thread worker_thread([&]() { + bridge->run(); // // When the sockets get closed, this application should // // terminate gracefully @@ -98,6 +127,9 @@ __cdecl TerminateProcess(GetCurrentProcess(), 0); }); + std::cout << "Finished initializing '" << plugin_location << "'" + << std::endl; + // Handle Win32 messages and X11 events on a timer, just like in // `GroupBridge::async_handle_events()`` main_context.async_handle_events([&]() { diff --git a/src/wine-host/utils.h b/src/wine-host/utils.h index d9683ef7..ef6e6355 100644 --- a/src/wine-host/utils.h +++ b/src/wine-host/utils.h @@ -18,16 +18,17 @@ #include "boost-fix.h" +#include #include #include +#ifndef NOMINMAX #define NOMINMAX -#define NOSERVICE -#define NOMCX -#define NOIMM -#define WIN32_LEAN_AND_MEAN +#define WINE_NOWINSOCK +#endif #include +#include #include #include @@ -63,6 +64,30 @@ class MainContext { */ void stop(); + /** + * Asynchronously execute a function inside of this main IO context and + * return the results as a future. This is used to make sure that operations + * that may involve the Win32 message loop are all run from the same thread. + */ + template + std::future run_in_context(F fn) { + std::packaged_task call_fn(std::move(fn)); + std::future response = call_fn.get_future(); + boost::asio::dispatch(context, std::move(call_fn)); + + return response; + } + + /** + * Run a task within the IO context. The difference with `run_in_context()` + * is that this version does not guarantee that it's going to be executed as + * soon as possible, and thus we also won't return a future. + */ + template + void schedule_task(F fn) { + boost::asio::post(context, std::move(fn)); + } + /** * Start a timer to handle events every `event_loop_interval` milliseconds. * @@ -89,8 +114,8 @@ class MainContext { } /** - * The raw IO context. Can and should be used directly for everything that's - * not the event handling loop. + * The raw IO context. Used to bind our sockets onto. Running things within + * this IO context should be done with the functions above. */ boost::asio::io_context context; diff --git a/subprojects/.gitignore b/subprojects/.gitignore index 21ced76b..e557da5e 100644 --- a/subprojects/.gitignore +++ b/subprojects/.gitignore @@ -2,3 +2,4 @@ # The above pattern doesn't match submodules /tomlplusplus +/vst3 diff --git a/subprojects/vst3.wrap b/subprojects/vst3.wrap new file mode 100644 index 00000000..19b3e6a6 --- /dev/null +++ b/subprojects/vst3.wrap @@ -0,0 +1,7 @@ +[wrap-git] +url = https://github.com/robbert-vdh/vst3sdk.git +# This is VST3 SDK v3.7.1_build_50 with the documentation and VSTGUI submodules +# removed +revision = e2fbb41f28a4b311f2fc7d28e9b4330eec1802b6 +clone-recursive = true +depth = 1 diff --git a/tools/patch-vst3-sdk.sh b/tools/patch-vst3-sdk.sh new file mode 100755 index 00000000..006a3cbf --- /dev/null +++ b/tools/patch-vst3-sdk.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# +# Patch the VST3 SDK and replace all MSVC-isms so it can be compiled with +# winegcc. We do it this way instead of modifying the SDK directly so we don't +# have to fork multiple repositories and keep them up to date. +# If anyone knows a better way to get the SDK to compile with Win32 supports +# under winegcc without having to modify it then please let me know, because I'd +# rather not have to do this. +# +# Usage: +# patch-vst3-sdk.sh + +set -euo pipefail + +sdk_directory=$1 +if [[ -z $sdk_directory ]]; then + echo "Usage:" + echo "patch-vst3-sdk.sh " + exit 1 +fi + +# Make sure all imports use the correct casing +find "$sdk_directory" -type f \( -iname '*.h' -or -iname '*.cpp' \) -print0 | + xargs -0 sed -i -E 's/^#include <(Windows.h|ShlObj.h)>$/#include <\L\1\E>/' + +# Use the proper libc functions instead of the MSVC intrinsics. These are also +# used in `fstring.cpp`, but there we will patch the entire file to use the +# standard POSIX/GCC string formatting facilities. +sed -i 's/\b_vsnprintf\b/vsnprintf/g;s/\b_snprintf\b/snprintf/g' "$sdk_directory/base/source/fdebug.cpp" + +# Use the attributes and types from GCC +sed -i 's/defined(__MINGW32__)/defined(__WINE__)/g' "$sdk_directory/pluginterfaces/base/ftypes.h" + +# There are some more places where the SDK includes better compatibility with +# GCC that we can use +# NOTE: We should **not** define __MINGW32__ globally, since that also breaks +# Wine's headers in headache inducing ways +sed -i 's/defined(__MINGW32__)/defined(__WINE__)/g' "$sdk_directory/public.sdk/source/common/systemclipboard_win32.cpp" + +# Use the string manipulation functions from the C standard library +sed -i 's/\bSMTG_OS_WINDOWS\b/0/g;s/\bSMTG_OS_LINUX\b/1/g' "$sdk_directory/base/source/fstring.cpp" +sed -i 's/\bSMTG_OS_WINDOWS\b/0/g;s/\bSMTG_OS_LINUX\b/1/g' "$sdk_directory/pluginterfaces/base/fstrdefs.h" +sed -i 's/\bSMTG_OS_WINDOWS\b/0/g;s/\bSMTG_OS_LINUX\b/1/g' "$sdk_directory/pluginterfaces/base/ustring.cpp" + +# `Windows.h` expects `wchar_t`, and the above defines will cause us to use +# `char16_t` for string literals. This replacement targets a very specific line, +# so if the SDK gets updated, this fails, and we're getting a ton of `wchar_t` +# related compile errors, that's why. The previous sed call will have replaced +# `SMTG_OS_WINDOWS` with a 0 here. +sed -i 's/^ #if 0$/ #if __WINE__/' "$sdk_directory/pluginterfaces/base/fstrdefs.h" + +# We'll need some careful replacements in the Linux definitions in `fstring.cpp` +# to use `wchar_t` instead of `char16_t`. +replace_char16() { + local needle=$1 + local filename=$2 + + wchar_version=${needle//char16_t/wchar_t} + sed -i "s/^$needle$/#ifdef __WINE__\\ + $wchar_version\\ +#else\\ + \0\\ +#endif/" "$filename" +} + +replace_char16 "using ConverterFacet = std::codecvt_utf8_utf16;" "$sdk_directory/base/source/fstring.cpp" +replace_char16 "using Converter = std::wstring_convert;" "$sdk_directory/base/source/fstring.cpp" +replace_char16 "using Converter = std::wstring_convert, char16_t>;" "$sdk_directory/pluginterfaces/base/ustring.cpp" + +# Don't try adding `std::u8string` to an `std::vector`. MSVC +# probably coerces them, but GCC doesn't +sed -i 's/\bgeneric_u8string\b/generic_string/g' "$sdk_directory/public.sdk/source/vst/hosting/module_win32.cpp" + +# libstdc++fs doesn't work under Winelib, for whatever reason that might be. +# We'll patch the Win32 module loading to use Boost.Filesystem instead. +sed -i 's/^#include <\(experimental\/\)\?filesystem>$/#include /' "$sdk_directory/public.sdk/source/vst/hosting/module_win32.cpp" +sed -i 's/^using namespace std\(::experimental\)\?;$/namespace filesystem = boost::filesystem;/' "$sdk_directory/public.sdk/source/vst/hosting/module_win32.cpp" +sed -i 's/\bfile_type::directory\b/file_type::directory_file/g' "$sdk_directory/public.sdk/source/vst/hosting/module_win32.cpp" +sed -i 's/\bp\.native ()/p.wstring ()/g' "$sdk_directory/public.sdk/source/vst/hosting/module_win32.cpp" + +# Meson requires this program to output something, or else it will error out +# when trying to encode the empty output +echo "Successfully patched '$sdk_directory' for winegcc compatibility" diff --git a/tools/yabridgectl/README.md b/tools/yabridgectl/README.md index c41ace1f..85b21562 100644 --- a/tools/yabridgectl/README.md +++ b/tools/yabridgectl/README.md @@ -13,11 +13,12 @@ from anywhere. All of the information below can also be found through ### Yabridge path -Yabridgectl will need to know where it can find `libyabridge.so`. By default it -will search for it in both `~/.local/share/yabridge` (the recommended -installation directory when using the prebuilt binaries) and in `/usr/lib`. You -can use the command below to override this behaviour and to use a custom -installation directory instead. +Yabridgectl will need to know where it can find `libyabridge-vst2.so` and +`libyabridge-vst3.so`. By default it will search for it in both +`~/.local/share/yabridge` (the recommended installation directory when using the +prebuilt binaries), in `/usr/lib` and in `/usr/local/lib`. You can use the +command below to override this behaviour and to use a custom installation +directory instead. ```shell yabridgectl set --path= @@ -26,11 +27,11 @@ yabridgectl set --path= ### Installation methods Yabridge can be set up using either copies or symlinks. By default, yabridgectl -will use the copy-based installation method since this will work with any VST -host. If you are using a DAW that supports individually sandboxed plugins such -as Bitwig Studio, then you can choose between using copies and symlinks using -the command below. Make sure to rerun `yabridgectl sync` after changing this -setting. +will use the copy-based installation method since this will work with any host, +and there's usually no reason to use symlinks anymore. If you are using a DAW +that supports individually sandboxed plugins such as Bitwig Studio, then you can +choose between using copies and symlinks using the command below. Make sure to +rerun `yabridgectl sync` after changing this setting. ```shell yabridgectl set --method= @@ -38,15 +39,18 @@ yabridgectl set --method= ### Managing directories -Yabridgectl can manage multiple Windows VST plugin install locations for you. To -add, remove and list directories, you can use the commands below. The status -command will show you yabridgectl's current settings and the installation status -for all of your plugins. +Yabridgectl can manage multiple Windows plugin install locations for you. +Whenever you run `yabridgectl sync` it will search these directories for VST2 +plugins and VST3 modules. To add, remove and list directories, you can use the +commands below. The status command will show you yabridgectl's current settings +and the installation status for all of your plugins. ```shell # Add a directory containing plugins -# Use the command from the next line to add the most common VST2 plugin directory +# Use the command from the next line to add the most common VST2 plugin directory: # yabridgectl add "$HOME/.wine/drive_c/Program Files/Steinberg/VstPlugins" +# VST3 plugins are located here: +# yabridgectl add "$HOME/.wine/drive_c/Program Files/Common Files/VST3" yabridgectl add # Remove a plugin location, this will ask you if you want to remove any leftover files from yabridge yabridgectl rm @@ -59,12 +63,13 @@ yabridgectl status ### Installing and updating Lastly you can tell yabridgectl to set up or update yabridge for all of your -plugins at once using the commands below. Yabridgectl will warn you if it finds -unrelated `.so` files that may have been left after uninstalling a plugin. You -can rerun the sync command with the `--prune` option to delete those files. If -you are using the default copy-based installation method, it will also verify -that your search `PATH` has been set up correctly so you can get up and running -faster. +VST2 and VST3 plugins at the same time using the commands below. Yabridgectl +will warn you if it finds unrelated `.so` files that may have been left after +uninstalling a plugin, or if it finds any unknown VST3 plugins in +`~/.vst3/yabridge`. You can rerun the sync command with the `--prune` option to +delete those files. If you are using the default copy-based installation method, +it will also verify that your search `PATH` has been set up correctly so you can +get up and running faster. ```shell # Set up or update yabridge for all plugins found under the plugin locations @@ -78,9 +83,14 @@ yabridgectl sync --force ## Alternatives If you want to script your own installation behaviour and don't feel like using -yabridgectl, then you could use one of the below bash snippets instead. This -approach is slightly less robust and does not perform any problem detection or -status reporting, but it will get you started. +yabridgectl, then you could use one of the below bash snippets instead to set up +yabridge for VST2 plugins. This approach is slightly less robust and does not +perform any problem detection or status reporting, but it will get you started. +Doing the same thing for VST3 plugins is much more complicated and it involves +[merged +bundle](https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html#mergedbundles) +with the Windows VST3 module symlinked in, so it's recommended to have +yabridgectl do that for you. ```shell # For use with symlinks diff --git a/tools/yabridgectl/src/actions.rs b/tools/yabridgectl/src/actions.rs index 2de63b7d..3e3a1db5 100644 --- a/tools/yabridgectl/src/actions.rs +++ b/tools/yabridgectl/src/actions.rs @@ -18,12 +18,12 @@ use anyhow::{Context, Result}; use colored::Colorize; +use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::{Path, PathBuf}; -use crate::config::{Config, InstallationMethod}; -use crate::files; -use crate::files::FoundFile; +use crate::config::{yabridge_vst3_home, Config, InstallationMethod, YabridgeFiles}; +use crate::files::{self, LibArchitecture, NativeFile}; use crate::utils; use crate::utils::{verify_path_setup, verify_wine_setup}; @@ -34,16 +34,15 @@ pub fn add_directory(config: &mut Config, path: PathBuf) -> Result<()> { } /// Remove a direcotry to the plugin locations. The path is assumed to be part of -/// `config.plugin_dirs`, otherwise this si silently ignored. +/// `config.plugin_dirs`, otherwise this is silently ignored. pub fn remove_directory(config: &mut Config, path: &Path) -> Result<()> { // We've already verified that this path is in `config.plugin_dirs` - // XXS: Would it be a good idea to warn about leftover .so files? config.plugin_dirs.remove(path); config.write()?; // Ask the user to remove any leftover files to prevent possible future problems and out of date // copies - let orphan_files = files::index_so_files(path); + let orphan_files = files::index(path).so_files; if !orphan_files.is_empty() { println!( "Warning: Found {} leftover .so files still in this directory:", @@ -84,7 +83,7 @@ pub fn list_directories(config: &Config) -> Result<()> { /// Print the current configuration and the installation status for all found plugins. pub fn show_status(config: &Config) -> Result<()> { let results = config - .index_directories() + .search_directories() .context("Failure while searching for plugins")?; println!( @@ -95,22 +94,35 @@ pub fn show_status(config: &Config) -> Result<()> { .map(|path| format!("'{}'", path.display())) .unwrap_or_else(|| String::from("")) ); - println!( - "libyabridge.so: {}", - config - .libyabridge() - .map(|path| format!("'{}'", path.display())) - .unwrap_or_else(|_| format!("{}", "".red())) - ); - println!("installation method: {}", config.method); + match config.files() { + Ok(files) => { + println!( + "libyabridge-vst2.so: '{}'", + files.libyabridge_vst2.display() + ); + println!( + "libyabridge-vst3.so: {}\n", + files + .libyabridge_vst3 + .map(|path| format!("'{}'", path.display())) + .unwrap_or_else(|| "".red().to_string()) + ); + } + Err(err) => { + println!("Could not find yabridge's files files: {}\n", err); + } + } + + println!("installation method: {}", config.method); for (path, search_results) in results { println!("\n{}:", path.display()); for (plugin, status) in search_results.installation_status() { let status_str = match status { - Some(FoundFile::Regular(_)) => "copy".green(), - Some(FoundFile::Symlink(_)) => "symlink".green(), + Some(NativeFile::Regular(_)) => "copy".green(), + Some(NativeFile::Symlink(_)) => "symlink".green(), + Some(NativeFile::Directory(_)) => "invalid".red(), None => "not installed".red(), }; @@ -154,80 +166,142 @@ pub struct SyncOptions { /// Set up yabridge for all Windows VST2 plugins in the plugin directories. Will also remove orphan /// `.so` files if the prune option is set. pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { - let libyabridge_path = config.libyabridge()?; - let libyabridge_hash = utils::hash_file(&libyabridge_path)?; - println!("Using '{}'\n", libyabridge_path.display()); + let files: YabridgeFiles = config.files()?; + let libyabridge_vst2_hash = utils::hash_file(&files.libyabridge_vst2)?; + let libyabridge_vst3_hash = match &files.libyabridge_vst3 { + Some(path) => Some(utils::hash_file(path)?), + None => None, + }; + + if let Some(libyabridge_vst3_path) = &files.libyabridge_vst3 { + println!("Setting up VST2 and VST3 plugins using:"); + println!("- {}", files.libyabridge_vst2.display()); + println!("- {}\n", libyabridge_vst3_path.display()); + } else { + println!("Setting up VST2 plugins using:"); + println!("- {}\n", files.libyabridge_vst2.display()); + } let results = config - .index_directories() + .search_directories() .context("Failure while searching for plugins")?; // Keep track of some global statistics + // The number of plugins we set up yabridge for let mut num_installed = 0; + // The number of plugins we create a (new) copy of `libyabridge-{vst2,vst3}.so` for let mut num_new = 0; + // The files we skipped during the scan because they turned out to not be plugins let mut skipped_dll_files: Vec = Vec::new(); - let mut orphan_so_files: Vec = Vec::new(); + // `.so` files and unused VST3 modules we found during scanning that didn't have a corresponding + // copy or symlink of `libyabridge-vst2.so` + let mut orphan_files: Vec = Vec::new(); + // All the VST3 modules we have set up yabridge for. We need this to detect leftover VST3 + // modules in `~/.vst3/yabridge`. + let mut yabridge_vst3_bundles: BTreeMap> = BTreeMap::new(); for (path, search_results) in results { num_installed += search_results.vst2_files.len(); - orphan_so_files.extend(search_results.orphans().into_iter().cloned()); + num_installed += search_results.vst3_modules.len(); + orphan_files.extend(search_results.vst2_orphans().into_iter().cloned()); skipped_dll_files.extend(search_results.skipped_files); if options.verbose { println!("{}:", path.display()); } + + // We'll set up the copies or symlinks for VST2 plugins for plugin in search_results.vst2_files { let target_path = plugin.with_extension("so"); - // We'll only recreate existing files when updating yabridge, when switching between the - // symlink and copy installation methods, or when the `force` option is set. If the - // target file already exists and does not require updating, we'll just skip the file - // since some DAWs will otherwise unnecessarily reindex the file. - // We check `std::fs::symlink_metadata` instead of `Path::exists()` because the latter - // reports false for broken symlinks. - if let Ok(metadata) = fs::symlink_metadata(&target_path) { - match (options.force, &config.method) { - (false, InstallationMethod::Copy) => { - // If the target file is already a real file (not a symlink) and its hash is - // the same as the `libyabridge.so` file we're trying to copy there, then we - // don't have to do anything - if metadata.file_type().is_file() - && utils::hash_file(&target_path)? == libyabridge_hash - { - continue; - } - } - (false, InstallationMethod::Symlink) => { - // If the target file is already a symlink to `libyabridge.so`, then we can - // skip this file - if metadata.file_type().is_symlink() - && target_path.read_link()? == libyabridge_path - { - continue; - } - } - // With the force option we always want to recreate existing .so files - (true, _) => (), - } - - utils::remove_file(&target_path)?; - }; - // Since we skip some files, we'll also keep track of how many new file we've actually // set up - num_new += 1; - match config.method { - InstallationMethod::Copy => { - utils::copy(&libyabridge_path, &target_path)?; - } - InstallationMethod::Symlink => { - utils::symlink(&libyabridge_path, &target_path)?; - } + if install_file( + options.force, + config.method, + &files.libyabridge_vst2, + Some(libyabridge_vst2_hash), + &target_path, + )? { + num_new += 1; } if options.verbose { println!(" {}", plugin.display()); } } + + // And then create merged bundles for the VST3 plugins: + // https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html#mergedbundles + if let Some(libyabridge_vst3_hash) = libyabridge_vst3_hash { + for module in search_results.vst3_modules { + // Check if we already set up the same architecture version of the plugin (since + // 32-bit and 64-bit versions of the plugin cna live inside of the same bundle), and + // show a warning if we come across any duplicates. + let already_installed_architectures = yabridge_vst3_bundles + .entry(module.target_bundle_home()) + .or_insert_with(BTreeSet::new); + if !already_installed_architectures.insert(module.architecture()) { + eprintln!( + "{}", + utils::wrap(&format!( + "{}: The {} version of '{}' has already been provided by another Wine \ + prefix, skipping '{}'\n", + "WARNING".red(), + module.architecture(), + module.target_bundle_home().display(), + module.original_module_path().display(), + )) + ); + + continue; + } + + // We're building a merged VST3 bundle containing both a copy or symlink to + // `libyabridge-vst3.so` and the Windows VST3 plugin + let native_module_path = module.target_native_module_path(); + utils::create_dir_all(native_module_path.parent().unwrap())?; + if install_file( + options.force, + config.method, + files.libyabridge_vst3.as_ref().unwrap(), + Some(libyabridge_vst3_hash), + &native_module_path, + )? { + num_new += 1; + } + + // We'll then symlink the Windows VST3 module to that bundle to create a merged + // bundle: https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html#mergedbundles + let windows_module_path = module.target_windows_module_path(); + utils::create_dir_all(windows_module_path.parent().unwrap())?; + install_file( + true, + InstallationMethod::Symlink, + &module.original_module_path(), + None, + &windows_module_path, + )?; + + // If `module` is a bundle, then it may contain a `Resources` directory with + // screenshots and documentation + // TODO: Also symlink presets, but this is a bit more involved. See + // https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html#win7preset + if let Some(original_resources_dir) = module.original_resources_dir() { + install_file( + false, + InstallationMethod::Symlink, + &original_resources_dir, + None, + &module.target_resources_dir(), + )?; + } + + if options.verbose { + println!(" {}", module.original_path().display()); + } + } + } + if options.verbose { println!(); } @@ -243,24 +317,53 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { println!(); } - // Always warn about leftover files sicne those might cause warnings or errors when a VST host + if let Ok(dirs) = fs::read_dir(yabridge_vst3_home()) { + orphan_files.extend( + dirs.filter_map(|entry| entry.ok()) + .map(|entry| entry.path()) + .filter_map(|path| { + // Add all directories in `~/.vst3/yabridge` to `orphan_files` if they are not a + // VST3 module we just created. We'll ignore symlinks and regular files since + // those are always user created. + match ( + yabridge_vst3_bundles.contains_key(&path), + utils::get_file_type(path), + ) { + (false, result @ Some(NativeFile::Directory(_))) => result, + _ => None, + } + }), + ); + } + + // Always warn about leftover files since those might cause warnings or errors when a VST host // tries to load them - if !orphan_so_files.is_empty() { + if !orphan_files.is_empty() { + let leftover_files_str = if orphan_files.len() == 1 { + format!("{} leftover file", orphan_files.len()) + } else { + format!("{} leftover files", orphan_files.len()) + }; if options.prune { - println!("Removing {} leftover .so files:", orphan_so_files.len()); + println!("Removing {}:", leftover_files_str); } else { println!( - "Found {} leftover .so files, rerun with the '--prune' option to remove them:", - orphan_so_files.len() + "Found {}, rerun with the '--prune' option to remove them:", + leftover_files_str ); } - for file in orphan_so_files { - let path = file.path(); - - println!("- {}", path.display()); + for file in orphan_files { + println!("- {}", file.path().display()); if options.prune { - utils::remove_file(path)?; + match file { + NativeFile::Regular(path) | NativeFile::Symlink(path) => { + utils::remove_file(path)?; + } + NativeFile::Directory(path) => { + utils::remove_dir_all(path)?; + } + } } } @@ -279,6 +382,8 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { return Ok(()); } + // The path setup is to make sure that the `libyabridge-{vst2,vst3}.so` copies can find + // `yabridge-host.exe` if config.method == InstallationMethod::Copy { verify_path_setup(config)?; } @@ -288,3 +393,54 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { Ok(()) } + +/// Create a copy or symlink of `from` to `to`. Depending on `force`, we might not actually create a +/// new copy or symlink if `to` matches `from_hash`. +fn install_file( + force: bool, + method: InstallationMethod, + from: &Path, + from_hash: Option, + to: &Path, +) -> Result { + // We'll only recreate existing files when updating yabridge, when switching between the symlink + // and copy installation methods, or when the `force` option is set. If the target file already + // exists and does not require updating, we'll just skip the file since some DAWs will otherwise + // unnecessarily reindex the file. We check `std::fs::symlink_metadata` instead of + // `Path::exists()` because the latter reports false for broken symlinks. + if let Ok(metadata) = fs::symlink_metadata(&to) { + match (force, &method) { + (false, InstallationMethod::Copy) => { + // If the target file is already a real file (not a symlink) and its hash is the + // same as that of the `from` file we're trying to copy there, then we don't have to + // do anything + if let Some(hash) = from_hash { + if metadata.file_type().is_file() && utils::hash_file(to)? == hash { + return Ok(false); + } + } + } + (false, InstallationMethod::Symlink) => { + // If the target file is already a symlink to `from`, then we can skip this file + if metadata.file_type().is_symlink() && to.read_link()? == from { + return Ok(false); + } + } + // With the force option we always want to recreate existing .so files + (true, _) => (), + } + + utils::remove_file(&to)?; + }; + + match method { + InstallationMethod::Copy => { + utils::copy(from, to)?; + } + InstallationMethod::Symlink => { + utils::symlink(from, to)?; + } + } + + Ok(true) +} diff --git a/tools/yabridgectl/src/config.rs b/tools/yabridgectl/src/config.rs index fbb6b979..0669e109 100644 --- a/tools/yabridgectl/src/config.rs +++ b/tools/yabridgectl/src/config.rs @@ -20,6 +20,7 @@ use anyhow::{anyhow, Context, Result}; use rayon::prelude::*; use serde_derive::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; +use std::env; use std::fmt::Display; use std::fs; use std::path::{Path, PathBuf}; @@ -28,20 +29,27 @@ use xdg::BaseDirectories; use crate::files::{self, SearchResults}; -/// The name of the config file, relative to `$XDG_CONFIG_HOME/CONFIG_PREFIX`. +/// The name of the config file, relative to `$XDG_CONFIG_HOME/YABRIDGECTL_PREFIX`. pub const CONFIG_FILE_NAME: &str = "config.toml"; /// The name of the XDG base directory prefix for yabridgectl, relative to `$XDG_CONFIG_HOME` and /// `$XDG_DATA_HOME`. const YABRIDGECTL_PREFIX: &str = "yabridgectl"; -/// The name of the library file we're searching for. -pub const LIBYABRIDGE_NAME: &str = "libyabridge.so"; +/// The name of yabridge's VST2 library. +pub const LIBYABRIDGE_VST2_NAME: &str = "libyabridge-vst2.so"; +/// The name of yabridge's VST3 library. +pub const LIBYABRIDGE_VST3_NAME: &str = "libyabridge-vst3.so"; /// The name of the script we're going to run to verify that everything's working correctly. pub const YABRIDGE_HOST_EXE_NAME: &str = "yabridge-host.exe"; /// The name of the XDG base directory prefix for yabridge's own files, relative to /// `$XDG_CONFIG_HOME` and `$XDG_DATA_HOME`. const YABRIDGE_PREFIX: &str = "yabridge"; +/// The path relative to `$HOME` that VST3 modules bridged by yabridgectl life in. By putting this +/// in a subdirectory we can easily clean up any orphan files without interfering with other native +/// plugins. +const YABRIDGE_VST3_HOME: &str = ".vst3/yabridge"; + /// The configuration used for yabridgectl. This will be serialized to and deserialized from /// `$XDG_CONFIG_HOME/yabridge/config.toml`. #[derive(Deserialize, Serialize, Debug)] @@ -49,12 +57,14 @@ pub struct Config { /// The installation method to use. We will default to creating copies since that works /// everywhere. pub method: InstallationMethod, - /// The path to the directory containing `libyabridge.so`. If not set, then yabridgectl will - /// look in `/usr/lib` and `$XDG_DATA_HOME/yabridge` since those are the expected locations for - /// yabridge to be installed in. + /// The path to the directory containing `libyabridge-{vst2,vst3}.so`. If not set, then + /// yabridgectl will look in `/usr/lib` and `$XDG_DATA_HOME/yabridge` since those are the + /// expected locations for yabridge to be installed in. pub yabridge_home: Option, - /// Directories to search for Windows VST plugins. We're using an ordered set here out of - /// convenience so we can't get duplicates and the config file is always sorted. + /// Directories to search for Windows VST plugins. These directories can contain both VST2 + /// plugin `.dll` files and VST3 modules (which should be located in `/drive_c/Program + /// Files/Common/VST3`). We're using an ordered set here out of convenience so we can't get + /// duplicates and the config file is always sorted. pub plugin_dirs: BTreeSet, /// The last known combination of Wine and yabridge versions that would work together properly. /// This is mostly to diagnose issues with older Wine versions (such as those in Ubuntu's repos) @@ -63,16 +73,16 @@ pub struct Config { } /// Specifies how yabridge will be set up for the found plugins. -#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum InstallationMethod { - /// Create a copy of `libyabridge.so` for every Windows VST2 plugin .dll file found. After - /// updating yabridge, the user will have to rerun `yabridgectl sync` to copy over the new - /// version. + /// Create a copy of `libyabridge-{vst2,vst3}.so` for every Windows VST2 plugin `.dll` file or + /// VST3 module found. After updating yabridge, the user will have to rerun `yabridgectl sync` + /// to copy over the new version. Copy, - /// This will create a symlink to `libyabridge.so` for every VST2 .dll file in the plugin - /// directories. As explained in the readme, this makes updating easier and remvoes the need to - /// modify the `PATH` environment variable. + /// This will create a symlink to `libyabridge-{vst2,vst3}.so` for every VST2 plugin `.dll` file + /// or VST3 module in the plugin directories. Now that yabridge also searches in + /// `~/.local/share/yabridge` since yabridge 2.1 this option is not really needed anymore. Symlink, } @@ -117,6 +127,23 @@ pub struct KnownConfig { pub yabridge_host_hash: i64, } +/// Paths to all of yabridge's files based on the `yabridge_home` setting. Created by +/// `Config::files`. +#[derive(Debug)] +pub struct YabridgeFiles { + /// The path to `libyabridge-vst2.so` we should use. + pub libyabridge_vst2: PathBuf, + /// The path to `libyabridge-vst3.so` we should use, if yabridge has been compiled with VST3 + /// support. + pub libyabridge_vst3: Option, + /// The path to `yabridge-host.exe`. This is the path yabridge will actually use, and it does + /// not have to be relative to `yabridge_home`. + pub yabridge_host_exe: PathBuf, + /// The actual Winelib binary for `yabridge-host.exe`. Will be hashed to check whether the user + /// has updated yabridge. + pub yabridge_host_exe_so: PathBuf, +} + impl Config { /// Try to read the config file, creating a new default file if necessary. This will fail if the /// file could not be created or if it could not be parsed. @@ -158,77 +185,97 @@ impl Config { .with_context(|| format!("Failed to write config file to '{}'", config_path.display())) } - /// Return the path to `libyabridge.so`, or a descriptive error if it can't be found. If - /// `yabridge_home` is `None`, then we'll search in both `/usr/lib` and - /// `$XDG_DATA_HOME/yabridge`. - pub fn libyabridge(&self) -> Result { - match &self.yabridge_home { + /// Find all of yabridge's files based on `yabridge_home`. For the binaries we'll search for + /// them the exact same way as yabridge itself will. + pub fn files(&self) -> Result { + let xdg_dirs = yabridge_directories()?; + + // First find `libyabridge-vst2.so` + let libyabridge_vst2: PathBuf = match &self.yabridge_home { Some(directory) => { - let candidate = directory.join(LIBYABRIDGE_NAME); + let candidate = directory.join(LIBYABRIDGE_VST2_NAME); if candidate.exists() { - Ok(candidate) + candidate } else { - Err(anyhow!( + return Err(anyhow!( "Could not find '{}' in '{}'", - LIBYABRIDGE_NAME, + LIBYABRIDGE_VST2_NAME, directory.display() - )) + )); } } None => { // Search in the two common installation locations if no path was set explicitely. // We'll also search through `/usr/local/lib` just in case but since we advocate // against isntalling yabridge there we won't list this path in the error message - // when `libyabridge.so` can't be found. + // when `libyabridge-vst2.so` can't be found. let system_path = Path::new("/usr/lib"); let system_path_alt = Path::new("/usr/local/lib"); - let user_path = yabridge_directories()?.get_data_home(); - for directory in &[system_path, system_path_alt, &user_path] { - let candidate = directory.join(LIBYABRIDGE_NAME); - if candidate.exists() { - return Ok(candidate); + let user_path = xdg_dirs.get_data_home(); + let directories = [system_path, system_path_alt, &user_path]; + let mut candidates = directories + .iter() + .map(|directory| directory.join(LIBYABRIDGE_VST2_NAME)); + match candidates.find(|directory| directory.exists()) { + Some(candidate) => candidate, + _ => { + return Err(anyhow!( + "Could not find '{}' in either '{}' or '{}'. You can override the \ + default search path using 'yabridgectl set --path='.", + LIBYABRIDGE_VST2_NAME, + system_path.display(), + user_path.display() + )); } } - - Err(anyhow!( - "Could not find '{}' in either '{}' or '{}'. You can override the default \ - search path using 'yabridgectl set --path='.", - LIBYABRIDGE_NAME, - system_path.display(), - user_path.display() - )) } - } + }; + + // Based on that we can check if `libyabridge-vst3.so` exists, since yabridge can be + // compiled without VST3 support + let libyabridge_vst3 = match libyabridge_vst2.with_file_name(LIBYABRIDGE_VST3_NAME) { + path if path.exists() => Some(path), + _ => None, + }; + + // `yabridge-host.exe` should either be in the search path, or it should be in + // `~/.local/share/yabridge` + let yabridge_host_exe = match which(YABRIDGE_HOST_EXE_NAME) + .ok() + .or_else(|| xdg_dirs.find_data_file(YABRIDGE_HOST_EXE_NAME)) + { + Some(path) => path, + _ => { + return Err(anyhow!("Could not locate '{}'.", YABRIDGE_HOST_EXE_NAME)); + } + }; + let yabridge_host_exe_so = yabridge_host_exe.with_extension("exe.so"); + + Ok(YabridgeFiles { + libyabridge_vst2, + libyabridge_vst3, + yabridge_host_exe, + yabridge_host_exe_so, + }) } - /// Return the path to `yabridge-host.exe`, or a descriptive error if it can't be found. This - /// will first search alongside `libyabridge.so` and then search through the search path. - pub fn yabridge_host_exe(&self) -> Result { - let libyabridge_path = self.libyabridge()?; - - let yabridge_host_exe_candidate = libyabridge_path.with_file_name(YABRIDGE_HOST_EXE_NAME); - if yabridge_host_exe_candidate.exists() { - return Ok(yabridge_host_exe_candidate); - } - - // Normally we wouldn't need the full absolute path to `yabridge-host.exe`, but it's useful - // for the error messages - Ok(which(YABRIDGE_HOST_EXE_NAME)?) - } - - /// Search for VST2 plugins in all of the registered plugins directories. This will return an - /// error if `winedump` could not be called. - pub fn index_directories(&self) -> Result> { + /// Search for VST2 and VST3 plugins in all of the registered plugins directories. This will + /// return an error if `winedump` could not be called. + pub fn search_directories(&self) -> Result> { self.plugin_dirs .par_iter() - .map(|path| files::index(path).map(|search_results| (path.as_path(), search_results))) + .map(|path| { + files::index(path) + .search() + .map(|search_results| (path.as_path(), search_results)) + }) .collect() } } /// Fetch the XDG base directories for yabridge's own files, converting any error messages if this /// somehow fails into a printable string to reduce boiler plate. This is only used when searching -/// for `libyabridge.so` when no explicit search path has been set. +/// for `libyabridge-{vst2,vst3}.so` when no explicit search path has been set. pub fn yabridge_directories() -> Result { BaseDirectories::with_prefix(YABRIDGE_PREFIX).context("Error while parsing base directories") } @@ -238,3 +285,10 @@ pub fn yabridge_directories() -> Result { pub fn yabridgectl_directories() -> Result { BaseDirectories::with_prefix(YABRIDGECTL_PREFIX).context("Error while parsing base directories") } + +/// Get the path where VST3 modules bridged by yabridgectl should be placed in. This is a +/// subdirectory of `~/.vst3` so we can easily clean up leftover files without interfering with +/// other native plugins. +pub fn yabridge_vst3_home() -> PathBuf { + Path::new(&env::var("HOME").expect("$HOME is not set")).join(YABRIDGE_VST3_HOME) +} diff --git a/tools/yabridgectl/src/files.rs b/tools/yabridgectl/src/files.rs index e3aac87e..8e6426d7 100644 --- a/tools/yabridgectl/src/files.rs +++ b/tools/yabridgectl/src/files.rs @@ -21,143 +21,276 @@ use anyhow::{Context, Result}; use lazy_static::lazy_static; use rayon::prelude::*; use std::collections::{BTreeMap, HashMap}; +use std::fmt::Display; use std::path::{Path, PathBuf}; use std::process::Command; use walkdir::WalkDir; -/// Stores the results from searching for Windows VST plugin `.dll` files and native Linux `.so` -/// files inside of a directory. These `.so` files are kept track of so we can report the current -/// installation status and to be able to prune orphan files. +use crate::config::yabridge_vst3_home; +use crate::utils::get_file_type; + +/// Stores the results from searching through a directory. We'll search for Windows VST2 plugin +/// `.dll` files, Windows VST3 plugin modules, and native Linux `.so` files inside of a directory. +/// These `.so` files are kept track of so we can report the current installation status of VST2 +/// plugins and to be able to prune orphan files. Since VST3 plugins have to be instaleld in +/// `~/.vst3`, these orphan files are only relevant for VST2 plugins. #[derive(Debug)] pub struct SearchResults { /// Absolute paths to the found VST2 `.dll` files. pub vst2_files: Vec, - /// `.dll` files skipped over during the serach. Used for printing statistics and shown when + /// Absolute paths to found VST3 modules. Either legacy `.vst3` DLL files or VST 3.6.10 bundles. + pub vst3_modules: Vec, + /// `.dll` files skipped over during the search. Used for printing statistics and shown when /// running `yabridgectl sync --verbose`. pub skipped_files: Vec, + /// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or /// a regular file. - pub so_files: Vec, + pub so_files: Vec, } +/// The results of the first step of the search process. We'll first index all possibly relevant +/// files in a directory before filtering them down to a `SearchResults` object. +#[derive(Debug)] +pub struct SearchIndex { + /// Any `.dll` file. + pub dll_files: Vec, + /// Any `.vst3` file or directory. This can be either a legacy `.vst3` DLL module or a VST + /// 3.6.10 module (or some kind of random other file, of course). + pub vst3_files: Vec, + /// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or + /// a regular file. + pub so_files: Vec, +} + +/// Native `.so` files and VST3 bundle directories we found during a search. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum FoundFile { +pub enum NativeFile { Symlink(PathBuf), Regular(PathBuf), + Directory(PathBuf), +} + +impl NativeFile { + /// Return the path of a found `.so` file. + pub fn path(&self) -> &Path { + match &self { + NativeFile::Symlink(path) | NativeFile::Regular(path) | NativeFile::Directory(path) => { + path + } + } + } +} + +/// VST3 modules we found during a serach. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Vst3Module { + /// Old, pre-VST 3.6.10 style `.vst3` modules. These are simply `.dll` files with a different p + /// refix. Even though this is a legacy format, almost all VST3 plugins in the wild still use + /// this format. + Legacy(PathBuf, LibArchitecture), + /// A VST 3.6.10 bundle, with the same format as the VST3 bundles used on Linux and macOS. These + /// kinds of bundles can come with resource files and presets, which should also be symlinked to + /// `~/.vst3/` + Bundle(PathBuf, LibArchitecture), +} + +impl Vst3Module { + /// The architecture of this VST3 module. + pub fn architecture(&self) -> LibArchitecture { + match &self { + Vst3Module::Legacy(_, architecture) | Vst3Module::Bundle(_, architecture) => { + *architecture + } + } + } + + /// Get the path to the Windows VST3 plugin. This can be either a file or a directory depending + /// on the type of moudle. + pub fn original_path(&self) -> &Path { + match &self { + Vst3Module::Legacy(path, _) | Vst3Module::Bundle(path, _) => path, + } + } + + /// Get the name of the module as a string. Should be in the format `Plugin Name.vst3`. + pub fn original_module_name(&self) -> &str { + match &self { + Vst3Module::Legacy(path, _) | Vst3Module::Bundle(path, _) => path + .file_name() + .unwrap() + .to_str() + .expect("VST3 module name contains invalid UTF-8"), + } + } + + /// Get the path to the actual `.vst3` module file. + pub fn original_module_path(&self) -> PathBuf { + match &self { + Vst3Module::Legacy(path, _) => path.to_owned(), + Vst3Module::Bundle(bundle_home, architecture) => { + let mut path = bundle_home.join("Contents"); + path.push(architecture.vst_arch()); + path.push(self.original_module_name()); + + path + } + } + } + + /// If this was a VST 3.6.10 style bundle, then return the path to the `Resources` directory if + /// it has one. + pub fn original_resources_dir(&self) -> Option { + match &self { + Vst3Module::Bundle(bundle_home, _) => { + let mut path = bundle_home.join("Contents"); + path.push("Resources"); + if path.exists() { + Some(path) + } else { + None + } + } + Vst3Module::Legacy(_, _) => None, + } + } + + /// Get the path to the bundle in `~/.vst3` corresponding to the bridged version of this module. + /// + /// FIXME: How do we solve naming clashes from the same VST3 plugin being installed to multiple + /// Wine prefixes? + pub fn target_bundle_home(&self) -> PathBuf { + yabridge_vst3_home().join(self.original_module_name()) + } + + /// Get the path to the `libyabridge.so` file in `~/.vst3` corresponding to the bridged version + /// of this module. + pub fn target_native_module_path(&self) -> PathBuf { + let native_module_name = match &self { + Vst3Module::Legacy(path, _) | Vst3Module::Bundle(path, _) => path + .with_extension("so") + .file_name() + .unwrap() + .to_str() + .expect("VST3 module name contains invalid UTF-8") + .to_owned(), + }; + + let mut path = self.target_bundle_home(); + path.push("Contents"); + path.push("x86_64-linux"); + path.push(native_module_name); + path + } + + /// Get the path to where we'll symlink `original_module_path`. This is part of the merged VST3 + /// bundle in `~/.vst3/yabridge`. + pub fn target_windows_module_path(&self) -> PathBuf { + let mut path = self.target_bundle_home(); + path.push("Contents"); + path.push(self.architecture().vst_arch()); + path.push(self.original_module_name()); + path + } + + /// If the Windows VST3 plugin we're bridging was in a VST 3.6.10 style bundle and had a + /// resources directory, then we'll symlink that directory to here so the host can access all + /// its original resources. + pub fn target_resources_dir(&self) -> PathBuf { + let mut path = self.target_bundle_home(); + path.push("Contents"); + path.push("Resources"); + path + } +} + +/// The architecture of a `.dll` file. Needed so we can create a merged bundle for VST3 plugins. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)] +pub enum LibArchitecture { + Dll32, + Dll64, +} + +impl Display for LibArchitecture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + LibArchitecture::Dll32 => write!(f, "32-bit"), + LibArchitecture::Dll64 => write!(f, "64-bit"), + } + } +} + +impl LibArchitecture { + /// Get the corresponding VST3 architecture directory name. See + /// https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html. + pub fn vst_arch(&self) -> &str { + match &self { + LibArchitecture::Dll32 => "x86-win", + LibArchitecture::Dll64 => "x86_64-win", + } + } } impl SearchResults { - /// For every found VST2 plugin, find the associated copy or symlink of `libyabridge.so`. The - /// returned hashmap will contain a `None` value for plugins that have not yet been set up. - /// - /// These two functions could be combined into a single function, but speed isn't really an - /// issue here and it's a bit more organized this way. - pub fn installation_status(&self) -> BTreeMap<&Path, Option<&FoundFile>> { - let so_files: HashMap<&Path, &FoundFile> = self + /// For every found VST2 plugin and VST3 module, find the associated copy or symlink of + /// `libyabridge-{vst2,vst3}.so`. The returned hashmap will contain a `None` value for plugins + /// that have not yet been set up. + pub fn installation_status(&self) -> BTreeMap> { + let so_files: HashMap<&Path, &NativeFile> = self .so_files .iter() .map(|file| (file.path(), file)) .collect(); - self.vst2_files + // Do this for the VST2 plugins + let mut installation_status: BTreeMap> = self + .vst2_files .iter() .map( |path| match so_files.get(path.with_extension("so").as_path()) { - Some(&file) => (path.as_path(), Some(file)), - None => (path.as_path(), None), + Some(&file_type) => (path.clone(), Some(file_type.clone())), + None => (path.clone(), None), }, ) - .collect() + .collect(); + + // And for VST3 modules. We have not stored the paths to the corresponding `.so` files yet + // because they are not in any of the directories we're indexing. + installation_status.extend(self.vst3_modules.iter().map(|module| { + ( + module.original_path().to_owned(), + get_file_type(module.target_native_module_path()), + ) + })); + + installation_status } /// Find all `.so` files in the search results that do not belong to a VST2 plugin `.dll` file. - pub fn orphans(&self) -> Vec<&FoundFile> { + /// We cannot yet do the same thing for VST3 plguins because they will all be installed in + /// `~/.vst3`. + pub fn vst2_orphans(&self) -> Vec<&NativeFile> { // We need to store these in a map so we can easily entries with corresponding `.dll` files - let mut orphans: HashMap<&Path, &FoundFile> = self + let mut orphans: HashMap<&Path, &NativeFile> = self .so_files .iter() - .map(|file| (file.path(), file)) + .map(|file_type| (file_type.path(), file_type)) .collect(); + for vst2_path in &self.vst2_files { orphans.remove(vst2_path.with_extension("so").as_path()); } - orphans.into_iter().map(|(_, file)| file).collect() + orphans.values().cloned().collect() } } -impl FoundFile { - /// Return the path of a found `.so` file. - pub fn path(&self) -> &Path { - match &self { - FoundFile::Symlink(path) => path, - FoundFile::Regular(path) => path, - } - } -} - -/// Search for Windows VST2 plugins and .so files under a directory. This will return an error if -/// the directory does not exist, or if `winedump` could not be found. -pub fn index(directory: &Path) -> Result { - // First we'll find all .dll and .so files in the directory - let (dll_files, so_files) = find_files(directory); - - lazy_static! { - static ref VST2_AUTOMATON: AhoCorasick = - AhoCorasick::new_auto_configured(&["VSTPluginMain", "main", "main_plugin"]); - } - - // THne we'll figure out which `.dll` files are VST2 plugins and which should be skipped by - // checking whether the file contains one of the VST2 entry point functions. The boolean flag in - // this vector indicates whether it is a VST2 plugin. - let dll_files: Vec<(PathBuf, bool)> = dll_files - .into_par_iter() - .map(|path| { - let exported_functions = Command::new("winedump") - .arg("-j") - .arg("export") - .arg(&path) - .output() - .context( - "Could not find 'winedump'. In some distributions this is part of a seperate \ - Wine tools package.", - )? - .stdout; - - Ok((path, VST2_AUTOMATON.is_match(exported_functions))) - }) - .collect::>()?; - - let mut vst2_files = Vec::new(); - let mut skipped_files = Vec::new(); - for (path, is_vst2_plugin) in dll_files { - if is_vst2_plugin { - vst2_files.push(path); - } else { - skipped_files.push(path); - } - } - - Ok(SearchResults { - vst2_files, - skipped_files, - so_files, - }) -} - -/// THe same as [index()](index), but only report found `.so` files. This avoids unnecesarily -/// filtering the found `.dll` files. -pub fn index_so_files(directory: &Path) -> Vec { - let (_, so_files) = find_files(directory); - - so_files -} - -/// Find all `.dll` and `.so` files under a directory. The results are a pair of `(dll_files, -/// so_files)`. -fn find_files(directory: &Path) -> (Vec, Vec) { +/// Find all `.dll`, `.vst3` and `.so` files under a directory. These results can be filtered down +/// to actual VST2 plugins and VST3 modules using `search()`. +pub fn index(directory: &Path) -> SearchIndex { let mut dll_files: Vec = Vec::new(); - let mut so_files: Vec = Vec::new(); + let mut vst3_files: Vec = Vec::new(); + let mut so_files: Vec = Vec::new(); // XXX: We're silently skipping directories and files we don't have permission to read. This // sounds like the expected behavior, but I"m not entirely sure. for (file_idx, entry) in WalkDir::new(directory) @@ -180,16 +313,144 @@ fn find_files(directory: &Path) -> (Vec, Vec) { match entry.path().extension().and_then(|os| os.to_str()) { Some("dll") => dll_files.push(entry.into_path()), + Some("vst3") => vst3_files.push(entry.into_path()), Some("so") => { if entry.path_is_symlink() { - so_files.push(FoundFile::Symlink(entry.into_path())); + so_files.push(NativeFile::Symlink(entry.into_path())); } else { - so_files.push(FoundFile::Regular(entry.into_path())); + so_files.push(NativeFile::Regular(entry.into_path())); } } _ => (), } } - (dll_files, so_files) + SearchIndex { + dll_files, + vst3_files, + so_files, + } +} + +impl SearchIndex { + /// Filter these indexing results down to actual VST2 plugins and VST3 modules. This will skip + /// all invalid files, such as regular `.dll` libraries. Will return an error if `winedump` + /// could not be found. + pub fn search(self) -> Result { + lazy_static! { + static ref VST2_AUTOMATON: AhoCorasick = + AhoCorasick::new_auto_configured(&["VSTPluginMain", "main", "main_plugin"]); + static ref VST3_AUTOMATON: AhoCorasick = + AhoCorasick::new_auto_configured(&["GetPluginFactory"]); + static ref DLL32_AUTOMATON: AhoCorasick = + AhoCorasick::new_auto_configured(&["Machine: 014C"]); + } + + let winedump = |args: &[&str], path: &Path| { + Command::new("winedump") + .args(args) + .arg(path) + .output() + .context( + "Could not find 'winedump'. In some distributions this is part of a seperate \ + Wine tools package.", + ) + .map(|output| output.stdout) + }; + let pe32_info = |path: &Path| winedump(&[], path); + let exported_functions = |path: &Path| winedump(&["-j", "export"], path); + + // We'll have to figure out which `.dll` files are VST2 plugins and which should be skipped + // by checking whether the file contains one of the VST2 entry point functions. This vector + // will contain an `Err(path)` if `path` was not a valid VST2 plugin. + let is_vst2_plugin: Vec> = self + .dll_files + .into_par_iter() + .map(|path| { + if VST2_AUTOMATON.is_match(exported_functions(&path)?) { + Ok(Ok(path)) + } else { + Ok(Err(path)) + } + }) + .collect::>()?; + + // We need to do the same thing with VST3 plugins. The added difficulty here is that we have + // to figure out of the `.vst3` file is a legacy standalone VST3 module, or part of a VST + // 3.6.10 bundle. We also need to know the plugin's architecture because we're going to + // create a univeral VST3 bundle. + let is_vst3_module: Vec> = self + .vst3_files + .into_par_iter() + .map(|module_path| { + let architecture = if DLL32_AUTOMATON.is_match(pe32_info(&module_path)?) { + LibArchitecture::Dll32 + } else { + LibArchitecture::Dll64 + }; + + if VST3_AUTOMATON.is_match(exported_functions(&module_path)?) { + // Now we'll have to figure out if the plugin is part of a VST 3.6.10 style + // bundle or a legacy `.vst3` DLL file. A WIndows VST3 bundle contains at least + // `.vst3/Contents//.vst3`, so + // we'll just go up a few directories and then reconstruct that bundle. + let module_name = module_path.file_name(); + let bundle_root = module_path + .parent() + .and_then(|arch_dir| arch_dir.parent()) + .and_then(|contents_dir| contents_dir.parent()); + let module_is_in_bundle = bundle_root + .and_then(|bundle_root| bundle_root.parent()) + .zip(module_name) + .map(|(path, module_name)| { + // Now reconstruct the path to the original file again as if it were in + // a bundle + let mut reconstructed_path = path.join(module_name); + reconstructed_path.push("Contents"); + reconstructed_path.push(architecture.vst_arch()); + reconstructed_path.push(module_name); + + reconstructed_path.exists() + }) + .unwrap_or(false); + + if module_is_in_bundle { + Ok(Ok(Vst3Module::Bundle( + bundle_root.unwrap().to_owned(), + architecture, + ))) + } else { + Ok(Ok(Vst3Module::Legacy(module_path, architecture))) + } + } else { + Ok(Err(module_path)) + } + }) + .collect::>()?; + + let mut skipped_files: Vec = Vec::new(); + + let mut vst2_files: Vec = Vec::new(); + for dandidate in is_vst2_plugin { + match dandidate { + Ok(path) => vst2_files.push(path), + Err(path) => skipped_files.push(path), + } + } + + let mut vst3_modules: Vec = Vec::new(); + for candidate in is_vst3_module { + match candidate { + Ok(module) => vst3_modules.push(module), + Err(path) => skipped_files.push(path), + } + } + + Ok(SearchResults { + vst2_files, + vst3_modules, + skipped_files, + so_files: self.so_files, + }) + } } diff --git a/tools/yabridgectl/src/main.rs b/tools/yabridgectl/src/main.rs index 649ce4e6..b21fc8cd 100644 --- a/tools/yabridgectl/src/main.rs +++ b/tools/yabridgectl/src/main.rs @@ -99,10 +99,10 @@ fn main() -> Result<()> { .arg( Arg::with_name("path") .long("path") - .about("Path to the directory containing 'libyabridge.so'") + .about("Path to the directory containing 'libyabridge-{vst2,vst3}.so'") .long_about( - "Path to the directory containing 'libyabridge.so'. If this is \ - not set, then yabridgectl will look in both '/usr/lib' and \ + "Path to the directory containing 'libyabridge-{vst2,vst3}.so'. If this \ + is not set, then yabridgectl will look in both '/usr/lib' and \ '~/.local/share/yabridge' by default.", ) .validator(validate_path) diff --git a/tools/yabridgectl/src/utils.rs b/tools/yabridgectl/src/utils.rs index 736015a6..7e70be33 100644 --- a/tools/yabridgectl/src/utils.rs +++ b/tools/yabridgectl/src/utils.rs @@ -25,11 +25,12 @@ use std::fs; use std::hash::Hasher; use std::os::unix::fs as unix_fs; use std::os::unix::process::CommandExt; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use textwrap::Wrapper; use crate::config::{self, Config, KnownConfig, YABRIDGE_HOST_EXE_NAME}; +use crate::files::NativeFile; /// (Part of) the expected output when running `yabridge-host.exe`. Used to verify that everything's /// working correctly. We'll only match this prefix so we can modify the exact output at a later @@ -47,6 +48,24 @@ pub fn copy, Q: AsRef>(from: P, to: Q) -> Result { }) } +/// Wrapper around [`std::fs::create_dir_all()`](std::fs::create_dir_all) with a human readable +/// error message. +pub fn create_dir_all>(path: P) -> Result<()> { + fs::create_dir_all(&path).with_context(|| { + format!( + "Error creating directories for '{}'", + path.as_ref().display(), + ) + }) +} + +/// Wrapper around [`std::fs::remove_dir_all()`](std::fs::remove_dir_all) with a human readable +/// error message. +pub fn remove_dir_all>(path: P) -> Result<()> { + fs::remove_dir_all(&path) + .with_context(|| format!("Could not remove directory '{}'", path.as_ref().display())) +} + /// Wrapper around [`std::fs::remove_file()`](std::fs::remove_file) with a human readable error /// message. pub fn remove_file>(path: P) -> Result<()> { @@ -66,6 +85,16 @@ pub fn symlink, Q: AsRef>(src: P, dst: Q) -> Result<()> { }) } +/// Get the type of a file, if it exists. +pub fn get_file_type(path: PathBuf) -> Option { + match path.symlink_metadata() { + Ok(metadata) if metadata.file_type().is_symlink() => Some(NativeFile::Symlink(path)), + Ok(metadata) if metadata.file_type().is_dir() => Some(NativeFile::Directory(path)), + Ok(_) => Some(NativeFile::Regular(path)), + Err(_) => None, + } +} + /// Hash the conetnts of a file as an `i64` using Rust's built in hasher. Collisions are not a big /// issue in our situation so we can get away with this. /// @@ -90,17 +119,21 @@ pub fn hash_file(file: &Path) -> Result { /// In the last case we'll just print a warning since we don't know how to invoke the shell as a /// login shell. This is needed when using copies to ensure that yabridge can find the host binaries /// when the VST host is launched from the desktop enviornment. +/// +/// This is a bit messy, and with yabridge 2.1 automatically searching in `~/.local/share/yabridge` +/// it's probably not really needed anymore, but it could still be useful in some edge case +/// scenarios. pub fn verify_path_setup(config: &Config) -> Result { // First we'll check `~/.local/share/yabridge`, since that's a special location where yabridge // will always search - if config::yabridge_directories() + let xdg_data_yabridge_exists = config::yabridge_directories() .map(|dirs| { dirs.get_data_home() .join(YABRIDGE_HOST_EXE_NAME) .is_executable() }) - .unwrap_or(false) - { + .unwrap_or(false); + if xdg_data_yabridge_exists { return Ok(true); } @@ -179,7 +212,7 @@ pub fn verify_path_setup(config: &Config) -> Result { reboot your system to complete the setup.\n\ \n\ https://github.com/robbert-vdh/yabridge#troubleshooting-common-issues", - config.libyabridge()?.parent().unwrap().display(), + config.files()?.libyabridge_vst2.parent().unwrap().display(), shell.bright_white(), "PATH".bright_white() )) @@ -230,13 +263,13 @@ pub fn verify_wine_setup(config: &mut Config) -> Result<()> { let mut wine_version = String::from_utf8(wine_version_output)?; wine_version.pop().unwrap(); - let yabridge_host_exe_path = config - .yabridge_host_exe() + let files = config + .files() .context(format!("Could not find '{}'", YABRIDGE_HOST_EXE_NAME))?; // Hash the contents of `yabridge-host.exe.so` since `yabridge-host.exe` is only a Wine // generated shell script - let yabridge_host_hash = hash_file(&yabridge_host_exe_path.with_extension("exe.so"))?; + let yabridge_host_hash = hash_file(&files.yabridge_host_exe_so)?; // Since these checks can take over a second if wineserver isn't already running we'll only // perform them when something has changed @@ -248,9 +281,9 @@ pub fn verify_wine_setup(config: &mut Config) -> Result<()> { return Ok(()); } - let output = Command::new(&yabridge_host_exe_path) + let output = Command::new(&files.yabridge_host_exe) .output() - .with_context(|| format!("Could not run '{}'", yabridge_host_exe_path.display()))?; + .with_context(|| format!("Could not run '{}'", files.yabridge_host_exe.display()))?; let stderr = String::from_utf8(output.stderr)?; // There are three scenarios here: