Add a VST3 host filter to obs-studio
Description
This PR implements a VST3 host for obs-studio, limited to audio effects (MIDI & instruments are excluded; they would require that obs supports MIDI natively which is not (yet?) the case). Sidechain is supported. Only one main input bus and one main output bus are supported.
This follows the announcement by Steinberg that VST3 SDK is from v3.8.0 licensed under a dual license (MIT or proprietary). Clarification was also done by Steinberg on request of obs-studio project about logo usage which used to be mandatory and was in contradiction to GPL v3. Ref: https://www.steinberg.net/developers/vstsdk/
VST3 has been written for windows, macOS and linux X11 platforms. Note that 3.8.0 adds also a preview Wayland support (contributed by Presonus). Ref: https://steinbergmedia.github.io/vst3_dev_portal/pages/Versions/Version+3.8.0.html Wayland support will be added later.
Screenshots:
VST3 with sidechain
Motivation and Context
VST3 are the current standard for audio filters so they are a must have for any tool doing audio processing. It's been a feature often requested by users.
How Has This Been Tested?
The following tests have been done on windows 11 23H2, macOS 15, linux Ubuntu 24.04 (X11 with proprietary nvidia drivers).
✅ Filter Lifecycle and Scene Integration
| Test Case | Description | Pass/Fail | Notes |
|---|---|---|---|
| Swap filters | Swap filters seamlessly (no crashes) | Pass | |
| Add/Remove During Streaming | Add/remove VST3 filter during stream/recording, no crash | Pass | |
| Enable/Disable Rapidly | Toggle filter on/off quickly, verify plugin state consistency | Pass | |
| Undo/Redo Filter Action | Undo/redo adding/removing filter, no leaks or hangs | Pass | |
| Scene Activation Behavior | Filter activates only when scene becomes active | Pass | |
| No leaks on exit | Exit OBS | Pass | |
| No crashes on exit | Exit OBS | Pass |
✅ Plugin Compatibility Tests
| Test Case | Description | Plugin tested | Pass/Fail | Notes |
|---|---|---|---|---|
| EQ Plugin | Load EQ plugin and check real-time parameter update | TDR Nova | Pass | |
| Compressor with Sidechain | Check gain reduction based on sidechain input | RoughRider 3 | Pass | |
| Reverb | Plugin with long tail, test render stability | BC Chorus, ValhallaSuperMassive | Pass | |
| Delay Plugin | Check delay feedback and sync accuracy | ValhallaSuperMassive | Pass | |
| Distortion Plugin | Ensure clipping/distortion plugin does not crash | IVGI | Pass | |
| GUI-only Plugin | Ensure plugin loads and filters audio even without processing | BC FreqANALYST, TDR Prism, | Pass | |
| GUI-only Plugin | Ensure plugin loads and filters audio even without processing | Voxengo Span | Pass | |
| Linear Phase EQ | Check that plugin initializes with proper latency reporting | Voxengo Marvel GEQ | Pass | |
| Linux Studio Plugins | Check that plugin bundle enumerates all plugins and works | Compressor etc | Pass |
✅ Sidechain and Source Enumeration
| Test Case | Description | Pass/Fail | Notes |
|---|---|---|---|
| Source List Correctness | Only audio sources are listed as sidechain candidates | Pass | |
| Source List Updates | Add/remove audio sources dynamically, check list refresh | Pass | |
| Source Selection Persistence | Restart OBS, sidechain selection should persist | Pass | |
| Sidechain Audio Matching | Inject tone into sidechain, confirm plugin envelope triggers | Pass | |
| Sidechain Removal | Delete active sidechain source, verify graceful degradation | Pass | |
| Mono Sidechain | Test with a Mono Sidechain like RoughRider 3 | Pass | |
| Stereo Sidechain | Test with a Stereo Sidechain like Melda MCompressor | Pass |
✅ Stress and Performance Tests
| Test Case | Description | Pass/Fail | Notes |
|---|---|---|---|
| Heavy Plugin + High CPU Load | Load demanding plugin under system stress | Pass | |
| Multiple Filter Instances | Add same plugin to multiple sources simultaneously | Pass | |
| Add several VST3s to same source | Add several VST3s to one source, change order, Enable/Disable | Pass | |
| Test with Steinberg hostchecker | Load VST3 host checker | Pass |
✅ Error Handling & Logging
| Test Case | Description | Pass/Fail | Notes |
|---|---|---|---|
| Plugin Load Failure | Attempt to load a corrupt/nonexistent plugin, check error log | Pass |
✅ Multiplatform support
| Test Case | Description | Pass/Fail | Notes |
|---|---|---|---|
| Windows 11 | Make all tests on Windows 11 | Pass | |
| macOS | Make all tests on macOS | Pass | |
| Ubuntu | Make all tests on Ubuntu linux | Pass | Tested LSP vst3 |
Types of changes
- New feature (non-breaking change which adds functionality)
Checklist:
- [x] My code has been run through clang-format.
- [x] I have read the contributing document.
- [x] My code is not on the master branch.
- [x] The code has been tested.
- [x] All commit messages are properly formatted and commits squashed where appropriate.
- [x] I have included updates to all appropriate documentation.
The Steinberg sdk is not conceived as a precompiled library but as a set of interfaces and optional helper classes which each plugin or host implements selectively. If you look at open source vst3 hosts (juce, ardour) or plugins (linux studio plugins), they all include the sdk for that reason. In our case I included only relevant parts of the sdk , not the whole of it, useful for a host (and it is already a mouthful indeed). So although it could be made into a static library, the SDK doesn't do so itself, it was not thought to provide one. Note that it is not header only in my case because some of the base sdk headers and some helpers headers have accompanying cpp implementations. So it is not a case where a single header needs inclusion from obs-deps. Maybe there is some magic from cmake which would allow it to work ?
In that case we'd just omit the compilation step, but it should still be provided as a "header-only" library via obs-deps (just like SIMDe and other third party dependencies).
Despite the name, as long as the target provided by the find module provides header files and source files as its INTERFACE_SOURCES property, they will be added to a target linking it just as if they were part of the source tree (but without the baggage of actually having to manage them in the same tree).
In that case we'd just omit the compilation step, but it should still be provided as a "header-only" library via
obs-deps(just like SIMDe and other third party dependencies).Despite the name, as long as the target provided by the find module provides header files and source files as its
INTERFACE_SOURCESproperty, they will be added to a target linking it just as if they were part of the source tree (but without the baggage of actually having to manage them in the same tree).
Ah, that was the info i was missing. If it can be done through cmake, that's nice.
In that case we'd just omit the compilation step, but it should still be provided as a "header-only" library via
obs-deps(just like SIMDe and other third party dependencies). Despite the name, as long as the target provided by the find module provides header files and source files as itsINTERFACE_SOURCESproperty, they will be added to a target linking it just as if they were part of the source tree (but without the baggage of actually having to manage them in the same tree).Ah, that was the info i was missing. If it can be done through cmake, that's nice.
Note that this is only necessary if you want the SDK source files to also appear with your plugin sources in the IDE. Usually you only provide the include path and just include the SDK's files where necessary.
But you can of course add them as sources and then in your target you can set the "FOLDER" property for all these files to put them visually in a subdirectory. Here's an example from the frontend project that looks at all the SOURCES on the project and then filters out headers, sources, and tells CMake to create directories based on their location relative to the main source directory: https://github.com/obsproject/obs-studio/blob/c025f210d36ada93c6b9ef2affd0f671b34c9775/frontend/CMakeLists.txt#L105-L117
Haven't checked whether SOURCES will implicitly include the INTERFACE_SOURCES of linked libraries, but that's how I'd approach it, if this is desired.
The devil's in the details. Your suggestion to move the sdk to obs-deps forgets about linux and that a patch is required for compatibility with Linux Studio Plugins (a similar patch is done by JUCE and Ardour). obs-deps doesn't cover linux while the plugin works on all our platforms (except flatpak because our flatpak doesn't allow /usr nor $HOME locations while they are the ones used by vst3s). Since the vst3 sdk is on github, we could pull it as a submodule, from a fork under the obsproject umbrella or directly from origin. We only use a subset though of the vst3 sdk which I patched, one of the patches being to allow compatibility with Linux Studio Plugins. I found on launchpad a vst3sdk package but it is out of date (version 3.7.10 from January 24). It is difficult to work around a workflow which has been thought of as having the sdk in-tree.
The devil's in the details. Your suggestion to move the sdk to obs-deps forgets about linux and that a patch is required for compatibility with Linux Studio Plugins (a similar patch is done by JUCE and Ardour).
What does the patch do? If it's necessary for Linux compatibility to begin with it should be upstreamed to the SDK repo and implemented by Steinberg then.
We are in no hurry to implement VST3 support in the project, and it's not OBS Studio's responsibility to contort itself to somehow "make it work". We need to aim for zero band-aids, in-tree patches, or other workarounds, as those require the most maintenance work and have the potential to break at even the smallest upstream change.
One of the patches could be upstreamed (zeroing of the buffers on creation). The other is not upstreamable. It ensures compatibility with some plugins which use messages but not on main ui thread. The sdk requires ui thread but for instance JUCE and Ardour don't enforce that prescription. Maybe this could ne negotiated with Steinberg to not be compulsory as current usage shows it is not enforced. Anyway, with or without the patches, the linux case is not solved by moving the vst3 sdk to obs-deps. So I don't see any alternative to in-tree sdk.
So what this reads to me is that the VST3 SDK is not really an SDK in the common sense, but more a "VST template": You take their source code and then write/change your own stuff around it, throw away the pieces you don't need and keep the ones you do?
So what this reads to me is that the VST3 SDK is not really an SDK in the common sense, but more a "VST template": You take their source code and then write/change your own stuff around it, throw away the pieces you don't need and keep the ones you do?
It is both. In order to implement either a host or a plugin, it would in principle be enough to use the classes defined in :
- base,
- interfaces. The public.sdk subfolder has however implementations of these interfaces. Sometimes it is not useful to reinvent the wheel and some other times it is. Hosts and plugins are free to use or not these implementations. Only what is in 'base' and 'interfaces' are compulsory. For the patches, since we have contacts with Steinberg, I'll discuss directly with them to see the potential for upstreaming.
Going forward i see these options:
- we drop support for linux , which allows us to pull the sdk from obs-deps. (Anyway support for flatpak is not possible.)
- we keep support for linux. But then either we leave the responsibility to users to pull the sdk and compile themselves vst3 plugin support or we find a solution which is not obs-deps. Like pulling a submodule.
From talks with @Fenrirthviti it was conveyed to me that linux support was desired but given that sdk difficulty, it seems a new arbitration is required.
I think the main issue with Linux seems to me that architecturally there is no need to provide a system package? Because it sounds like every VST is effectively compiled "on top" of the SDK code, so the SDK becomes the VST.
And because of that, no VST would ever need to dynamically link to a system-wide VST library.
I'll need to marinate on that.
@tytan652 what's your take? If we need to carry a dependency around in-tree (despite all our efforts to stop doing that) because Linux needs it and thus cannot be cordoned off to obs-deps, where should we put it?
Using obs-deps for Linux would probably only work for header-only libraries (as those would be distro-independent), but then we could also potentially make the CEF setup work the same way it does on Windows and macOS?
And because of that, no VST would ever need to dynamically link to a system-wide VST library.
Correct. All vst3s and hosts are compiled with the sdk. There is no lib in system to which there is a link.
This is true for all platforms supported by the vst3 sdk.
what's your take? If we need to carry a dependency around in-tree (despite all our efforts to stop doing that) because Linux needs it and thus cannot be cordoned off to
obs-deps, where should we put it?
VST3 assumes two system path:
/usr/lib/vst3/usr/local/lib/vst3
According to those paths:
- Does VST3 support Ubuntu ?
- No, binaries in the lib folder needs to be in the corresponding triplets subfolder so
/usr/lib/x86_64-linux-gnu/vst3.
- No, binaries in the lib folder needs to be in the corresponding triplets subfolder so
- Does VST3 support Flatpak ?
- No since we are not able to point the correct extension path through an environment variable like we did for VST2. And we can't use the
APPFOLDERenvironment variable with a custom path unless the vst is loaded in a isolated process since user should not be able to mess with the variable.
- No since we are not able to point the correct extension path through an environment variable like we did for VST2. And we can't use the
- Which distro VST3 actually support ?
- A distro where
/usr/libis used directly to store binaries matching the installation architecture, so Arch Linux and Gentoo… (I wish I was joking)
- A distro where
- But
$HOME/.vst3?- Only non-sandboxed packages allows it since there is no risk of escaping the sandbox and does not mean that the distro is properly supported.
None of our official packages are correctly supported. When it comes to adding features, I heavily prefer if the Flatpak has support since it has the biggest coverage (most distros including immutable).
Throwing the VST3 SDK in obs-deps-buildstream is not inconceivable, same to the PPA (it has already its own build of libajantv2). But in-tree should definitively be a no-go in my opinion.
PS: For the sake of sanity and simplicity I do not take in account unofficial packaging, it will be on the concerned packager⋅s.
When it comes to adding features, I heavily prefer if the Flatpak has support since it has the biggest coverage (most distros including immutable).
Throwing the VST3 SDK in obs-deps-buildstream is not inconceivable, same to the PPA (it has already its own build of libajantv2). But in-tree should definitively be a no-go in my opinion.
This bars complete linux support (which I really don't mind ;) ). Because even if you throw the vst3 sdk in buildstream, this doesn't solve the fact that currently the vst3 ecosystem wants install in locations which are not accessible to flatpak, as you summarized. Imo flatpak is a flat no in terms of support of vst3 , at least currently. To change that, it would require Steinberg to allow alternate locations than /usr or $HOME , and then plugin authors to allow these new locations. So in the short term, it is illusory to hope to get flatpak working with vst3.
This leaves only linux support away from flatpak, say ubuntu. This means either one modifies obs-deps to accommodate vst3 sdk for linux or we pull it in deps folder as a submodule or in-tree in the obs-vst3 folder.
Maybe the simplest is to plainly remove support for linux. I have no preference tbh.
VST3 assumes two system path:
/usr/lib/vst3/usr/local/lib/vst3According to those paths:
- Does VST3 support Ubuntu ?
- No, binaries in the lib folder needs to be in the corresponding triplets subfolder so
/usr/lib/x86_64-linux-gnu/vst3.
No this is incorrect. You are assuming the host scans the system lib folders. The host scans the above mentioned folders in addition to $HOME/.vst3 where vst3 plugins get installed. So ubuntu can definitely be supported. And actually i compiled the plugin and tested it on ubuntu 24.04 on several linux vst3s (LSP, RoughRider 3 ...) and also windows vst3s through wine and yabridge.
Update
- The SDK is no longer in-tree (see https://github.com/obsproject/obs-deps/pull/303 ). A finder for the SDK has been added.
- macos, windows: the SDK is pulled from obs-deps (from my obs-deps fork; a PR to obs-deps is pending).
- linux ubuntu: it is left to the user to specify a cmake VST3SDK_PATH.
- CI has been modified for ubuntu runner so that the SDK is pulled and added to cmake configuration stage.
- Flatpak: there will be no support since the vst3s are by default installed in locations unavailable to flatpak (/usr and $HOME). No custom location is offered by the SDK through an env var unfortunately.
- Linux Wayland: SDK 3.8.0 adds Wayland support ~~but I am not aware of any plugins implementing it, due to how recent it is. I will add Wayland support later,~~ when I can test against a VST3. Edit: Steinberg engineers pointed me to a VST3 implementing Wayland. There is a problem though with this preview feature. The SDK does not allow to discriminate yet wayland and/or X11 support when scanning the vst3s. So Wayland vst3 list would include X11 only vst3s ... That's obviously detrimental and an absolute block on our side.
- SDK patches: All dropped. I integrated one into my code; I dropped the other after the one plugin requiring the patch (LSP on linux, which bundles about 20-30 vst3s on linux, mac - LSP is well known in linux world and must be supported imo) decided to fix their own code. So the SDK needs no patches and can be used in vanilla flavour.
I address previous comments now:
Also as this is a "fresh" plugin, we should do things the right way:
Use platform-specific implementation files and platform-independent header files.
- This allows the API(s) to be generic and called in a generic fashion and then the implementation of that generic function does the platform specific thing
- E.g. there should be an implementation of
getDefaultSearchPathsin a file calledVST3Scanner_Windows.cpp,VST3Scanner_macOS.cpp,VST3Scanner_Linux.cpp, etc. implementing the general definition of the same function.
- I agree in principle (see the 3 separate implementations of VST3EditorWindow class for all 3 platforms).
- But for the specific case of
getDefaultSearchPaths, i disagree; this is a really small amount of code and nothing is gained by splitting such basic code. It's clearer to me in a single place. Should Steinberg add new locations, I'll do it in one place after having consulted this one page: https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/Locations+Format/Plugin+Locations.html
Each C++ class has to be put into its own pair of header and implementation file. This avoids unnecessary recompilations of every class just because a single detail of one class is changed in
VST3Plugin.h
- This also makes reviews and maintenance easier, as everything is properly encapsulated and separation of concerns is replicated by separation of implementation.
- agreed; i've split up as far as possible the various classes. That being said, the reason that everything was in a single header is that VST3Plugin and VST3HostApp could have really been a single class since I have chosen to have one host per plugin instead of a single host for several plugins. But having 2 classes made more sense in terms of the SDK logic conceived for a DAW. The reason I didn't want a single host is to have an easier management on linux of the RunLoop (which is an event loop required by the SDK on linux); instead of the RunLoop managing several plugins, each plugin has its own RunLoop and VST3Host. Another advantage is that, should a plugin cause issues, it minimizes the risks of propagating elsewhere.
Split up implementation of the
libobssource API and thenlibobsproperties API into separate files (seemac-avcaptureas an example). In general opt for shorter source files that implement a single aspect of the API instead of combining everything into a single "obs-whatever.c" file. In general I find it better to have a plugin be split up into these parts:
- Implementation of the module API
- Implementation(s) of a specific "source" API, one for each type (source, filter, output, etc.)
- Implementation(s) of the property API for each source type
- Implementation of the actual self-contained object that manages resources (for anything but a pure C plugin that would mean the C++/ObjC/Swift instance of an object)
- Again, see
mac-avcaptureor this Swift-based exampleEvery time a source file has a long "line separating comment", it means the file is too long and contains too much - split it up per the comment above.
- I suppose each dev has their preferred workflow. I personally disagree with these prescriptions. They are not part of the coding guidelines for obs-studio. They should have been first debated either in the dev team or in the core team and then officialized in the guidelines.
- My personal preference is to have all the 'source info' callbacks in the same place because for instance the Properties callbacks can call other functions associated with update ... I don't want to juggle between several files.
- Do not use
_prefixunderscores for "private" variables in C++, those names are reserved by the standard library. Always usepostfix_underscores.
- fixed.
Use our modern header include order (see the updated files in
frontendfor examples`:
#include "working_directory_file.h"(file in the same directory as the one doing the inclusion)#include <first_party_file.h>(file provided by some other part of the project or a first-party shared library)#include <third_party/file.h>(file provided by a third party library)#include <standard_library.h>(file provided by the C or C++ standard library, with C++ includes coming first)Also use
#importby default for ObjC/ObjC++
- fixed apart from an issue with Qt inclusion on linux when X11 is also included; Qt must be included before anything having X11/Xlib.h due to some redefinitions causing havoc.
Update (11/22/25)
- I have removed all mutexes in the audio thread (except those related to sidechain update), the audio pipeline is now lock free, which is much better. I had initially parted from Ross Bencina's famous "time waits for nothing" rule for audio programming (http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing) but managed to follow it eventually.
- I have modified the structure of the classes to be closer to Steinberg SDK, with a single global IHostApplication and on linux a single IRunLoop shared by all VST3s loaded by the host.
General architecture of obs-vst3 plugin
obs-vst3 provides a full VST3 host inside OBS Studio, analogous to what DAWs implement.
One major difference with DAWs though is that VST3s can be hot-swapped in obs, while in DAWs in general, they are applied or removed sequentially.
The Steinberg SDK is quite complex and it took me some time and pain to understand its structure. The EasyVST host implementation was a great help at start because it helped me understand the basics. One reason which makes the SDK tough is that it defines:
- COM-like pure abstract interfaces (in SDK
pluginterfaces) - Convenience C++ implementations (in
public.sdk/source/vst/)
It took me a while to get that the public.sdk classes are not of mandatory use and that to understand the SDK one had to focus on the 'plugininterfaces'; the public.sdk offers though SDK compliant implementations so reading it complements the sometimes unclear docs for the vst interfaces. While some hosts reimplement everything from scratch (like Ardour or JUCE), obs-vst3 reuses the SDK’s helper classes as they are SDK compliant, maintained, and significantly reduce host-side complexity.
For a general SDK overview:
https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/API+Documentation/Index.html
Host requirements:
https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/Host+Requirements/Index.html
obs-vst3 implements all mandatory interfaces except MIDI (OBS has no MIDI subsystem). It also does not implement IDataExchangeHandler (introduced in SDK 3.7.9), which is optional.
A VST3 plugin has two logical parts:
- Processor:
IComponent+IAudioProcessor(the split is because initially, the SDK authors planned a later addition of IVideoProcessor; IComponent would then have been the common parts. This never materialized though). Many plugins implement both in the same polymorphic class. - Controller:
IEditController(optional, provides the GUI and parameter logic when the VST3 has a GUI).
Processor ↔ controller communication goes through the host via IConnectionPoint, IMessage, and IAttributeList.
This exchange must happen on the UI thread, never on the audio thread in order to avoid slow downs and audio glitches.
The main objects in obs-vst3 are:
- vst3_audio_data – associated with each OBS filter instance
- VST3Plugin – the wrapper implementing VST3 lifecycle & processing
- VST3ComponentHolder – Steinberg edit-handler implementation
- VST3HostApp – global IHostApplication
- VST3Scanner – which discovers installed VST3 plugins
1. SDK
The VST3 SDK is designed for in-tree inclusion (headers + .cpp helpers, no libs are ever compiled).
OBS uses a custom CMake Finder to locate and validate the SDK through the VST3SDK_PATH var.
- On Windows and macOS, the SDK is shipped via
obs-deps - On Linux, the SDK must be included manually through the VST3SDK_PATH var.
CI was modified to pull the SDK either directly (linux) or through obs-deps.
2. Module loading
At module load time (obs_module_load and obs_module_post_load), these steps are performed in plugin-main.cpp:
1. Global host creation
A single instance of IHostApplication is created.
It provides the shared host context for every VST3Plugin instance.
Linux-specific
On Linux, IHostApplication also provides an IRunLoop implementation, required because VST3 assumes a host-provided event loop.
obs-vst3 implements this using Qt’s QSocketNotifier and QTimer inside a Steinberg-compatible wrapper.
The runloop provides:
- routing of X11/XEmbed window events
- timers for plugin-scheduled tasks
- FD handlers (monitoring read-ready file descriptors)
2. VST3 scanning
Scanning happens in a background thread because it can take several seconds.
Two scanning methods exist:
- Fast path:
moduleinfo.json(SDK ≥ 3.7.5) - Slow path: load the VST3 module and inspect its factory
Most plugins still do not ship moduleinfo.json, so the slow path dominates.
Only audio effects (kVstAudioEffectClass) are enumerated.
Plugins are identified by their unique 16-byte FUID.
Wayland note
SDK 3.8.0 adds Wayland support, but few real plugins support it.
The SDK currently cannot differentiate X11 vs Wayland support, so Wayland hosts may list plugins that will not run. A fix is under discussion with Steinberg.
Flatpak note
Flatpak is not supported because the locations of the VST3s on linux are not compatible (/usr or $HOME). There was a discussion with Steinberg engineers to allow for a VST3_PATH env var to enable custom install paths but they were quite resistant to the idea, which according to them caused issues with VST2. See discussion in this GH issue:
In the past the user had the chance to specify unlimited recursively scanned folders to the VST 2 search path.
This led to a massive amount of support work for plug-in
3. Filter creation and lifecycle
The filter implementation is in obs-vst3.cpp.
A vst3_audio_data instance is created per-filter.
Three threads interact:
- UI thread – creation, update, showing/hiding the plugin GUI, Properties
- Audio thread – real-time audio processing
- Video thread – sidechain audio capture
3.1 Creation & update
vst3_create() allocates the structure, and vst3_update() loads OBS settings and initializes the VST3Plugin.
OBS allows hot-swapping plugins, unlike most DAWs.
Sidechain support must also be detected and handled dynamically.
Real-time safety
One issue which influenced the architecture is the fact that hot swap of VST3s must be possible in obs; this is quite different from DAWs in general. Also, not all VST3s support a sidechain. This should be reflected in the update function through various update paths. As always when several threads are involved, the main issue is use after free of deleted pointers. The obvious solution to use mutexes all around is a bad idea with audio processing. The cardinal rule of audio programming is Ross Bencina's famous: "Time waits for nothing" (see his blog post) So in order to solve this issue, we opted for a shared pointer for the VST3Plugin class; this ensures that deletion won't happen before all threads are done with the ptr. We use also std::atomic bools instead of mutexes to deactivate the plugin whenever required (when there is a swap for instance or at obs exit).
3.2 Audio pipeline
In order to implement a lock free audio pipeline, we are using deques (double ended queues) all around. (Note: I am using OBS’s deque instead of std::deque for its much lower latency jitter (it is faster also). Its flat ring-buffer design avoids the segmentation and unpredictable branching of std::deque, giving more stable timing in real-time audio processing.)
The pipeline is as follows:
- at each audio tick (every 1024 samples usually), the filter_audio callback pushes the parent source audio samples into the input deque. In parallel an info_buffer deque stores timestamp info and the number of frames (1024).
- the preprocess_input function pops 480 samples from input deque to temp buffers, where they are copied into the VST3 input buffers;
- the VST3 process function is then called for these 480 samples;
- both preprocess and process are called repeatedly until there is less than 480 samples in the deque.
- the figure of 480 samples corresponds to 10 ms of audio at 48kHz sample rate.
- one retrieves processed output samples from the VST3 output buffers, and copies them into the output deque.
- the filter_audio function passes 1024 processed frames from the output deque to libobs.
3.3 Sidechain support
The basic logic we use is from obs-filters/compressor.c. The main issue is that the sidechain audio capture is done in the video thread, not the audio thread, through the video_tick callback. Any sidechain source swap is done then in two stages:
- in the usual update callback in the UI thread,
- in the video_tick callback in the video thread after allowing for at most a 3 sec delay to enforce the swap.
VST3s complicate the picture when they are hot-swapped because not all VST3s support sidechains ... So it had to be taken into account in the update callback, which was complex to write.
The sidechain audio pipeline is then:
- audio capture in the video_tick callback in the video thread (sidechain_capture function), which feeds a sc_input_deque,
- copy of this sidechain audio into the VST3 sidechain buffers in the audio_thread.
These audio operations are done in a lock-free manner, without mutexes, to minimize the friction and audio glitches. But the update of sidechains relies on a mutex as in compressor.c.
3.4 Properties
The properties dialog shows:
- VST3 plugin list
- Show/Hide GUI button
- Sidechain source dropdown (if plugin supports aux input)
- Warning if plugin has no GUI
- Error message in case of failed plugin load
The VST3s will usually fail to load for the following reasons:
- obs has enabled surround sound (say, 5.1) but the VST3 doesn't support the speaker layout,
- buggy VST3s, The exact reason is given in obs logs if we can determine it.
The properties refresh when one swaps VST3s is done in the on_vst3_changed_cb function.
4. The VST3Plugin class
A VST3Plugin wraps:
- IComponentHolder
- IComponent
- IAudioProcessor
- IEditController
- IPlugView (optional GUI)
It uses the global VST3HostApp (IHostApplication) for context.
The IHostApplication interface is responsible for advertising the host
capabilities and on linux, provides an IRunLoop.
4.1 Initialization (VST3Plugin::init)
First, the VST3Plugin class is constructed along with an IComponentController context which will be used for GUI editing. The global IHostApplication context is retrieved here.
The VST3Plugin::init function is then called with a given GUID for the selected VST3 as well as with basic audio settings (ex: sample rate). The following steps are then done:
- set parameters for Vst::ProcessSetup & Vst::ProcessContext (channels, sample rate, number of samples), which will be used during the audio processing phase,
- create a Module class, which is a convenience class from the hosting public.sdk; the module allows access to a PluginFactory class which holds info on all the VST3s found in the system. (Yeah, this is kind of bloated...)
- we create a PlugProvider class with the desired GUID. That class provided by the hosting public SDK takes care of creating, initializing and hooking the IComponent, IAudioProcessor and IEditController.
- we retrieve the pointers to the three interfaces and pass the IComponentHolder as context to the IEditController.
- we scan the audio buses. The SDK distinguishes between main and aux buses, as well as input or output buses. We only enable 1 main input bus, 1 main output bus and maybe 1 aux bus for sidechain if such a bus is offered. The other buses are disabled.
- during the bus scan, if no input bus is detected, an error is issued. The filtering is then disabled through the atomic bool bypass from vst3_audio_data struct.
- we then try to pass a channel layout to the buses; if they don't allow it, the filtering is disabled.
- if all has gone well, we call the setupProcessing function and pass the Vst::ProcessSetup params.
- if the params setup succeeds, we call the prepare function which will initialize the VST3 buffers.
- we silence the initial output buffers for safety.
- if there's no error, the VST3 can then be activated through the setActive(true) call.
4.2 Audio processing
This is done in OBS audio thread by calling these functions:
- preprocess: which will take care of passing param changes from GUI to DSP,
- process: which does the audio processing, by passing the VST3 buffers filled by obs in filter_audio pipeline,
- postprocess: which takes care of transmitting param changes from DSP to GUI
4.3 GUI
It is odd but the SDK does not provide a way to know whether a VST3 provides a GUI or not without trying to load one. In the VST3 creation path in obs-vst3 update function, we therefore try to call VST3Plugin::createView which will try to create an IPlugView, the GUI base interface in the SDK.
If a view exists, a VST3EditorWindow object is created at the first call to the VST3Plugin::showEditor() function and an OS specific window is created:
- Win32: HWND
- macOS: NSPanel
- Linux: X11/XEmbed window
An OS specific implementation for VST3EditorWindow was done, to stick to the SDK editor host sample. Qt could have been advantageous but in learning the SDK it was simpler to proceed like that. A pro of having native implementations is we avoid the Qt superlayer and its bugs, and have direct control in case of issues... the con is that this requires more work (linux X11 support was quite complex).
The SDK requires to hook the IPlugview interface to IPlugFrame which deals with resizing and DPI-awareness.
I experienced issues with some VST3s which don't like repeated opening and destruction of windows. In order to alleviate the issue I opted to hide GUI windows instead of closing them, except at the VST3 destruction.
Close events are intercepted natively:
- Win32:
WM_CLOSE - macOS:
windowShouldClose - X11:
_WM_DELETE_WINDOWorDestroyNotify
For GUI editing, note that an IComponentHandler interface is required; it is used when editing is done in the GUI and to restart the IComponent and IAudioProcessor, which is mandatory to implement. This is implemented in the VST3ComponentHolder class, which could have been bundled into the VST3Plugin class but was kept separate for clarity.
4.4 VST3 State save & load
OBS stores two independent VST3 state blobs:
- Component state → via
IComponent::getState/setState - Controller state → via
IEditController::getState/setState
On save:
- pull both states from the plugin → MemoryStream → hex-encode into OBS settings
On load:
- decode obs settings → write component + controller state back to the plugin