A thread-safe, cross-platform C# library for hosting VST3 plugins — and, on macOS, Audio Units as well. Built for audio applications where the UI thread, the audio thread, and the plugin's native runtime must never block each other. The native backend is powered by JUCE, providing broad plugin compatibility across all supported platforms.
- Thread-safe by design — dedicated plugin thread, lock-free UI→audio parameter queue, atomic state machine
- Full VST3 support — instruments, effects, MIDI-only plugins, parameter automation, transport context
- Audio Unit support on macOS — AUv2
.componentbundles alongside VST3, through the same API - Sub-second plugin scanner — finds VST3 bundles and registry-only AudioUnits without loading them; opt into a full scan when you need channel counts
- Cross-platform native editors — Win32 (STA), Cocoa (GCD main thread), X11 (dedicated event loop)
- Zero allocation audio path — pre-pinned buffers, no heap allocation inside
ProcessAudio - Queryable plugin state —
NotLoaded → Loaded → Ready ↔ Processing | Error - Automatic platform detection — native library resolved at runtime for
win-x64,osx-arm64,linux-x64and more
dotnet add package OwnVst3Hostusing OwnVST3Host;
using OwnVST3Host.NativeWindow;
// 1. Create the wrapper — starts the dedicated plugin thread immediately.
await using var plugin = new ThreadedVst3Wrapper();
// 2. Load and initialize on the plugin thread; the UI thread is never blocked.
bool loaded = await plugin.LoadPluginAsync("/Library/Audio/Plug-Ins/VST3/MyPlugin.vst3");
bool ready = await plugin.InitializeAsync(sampleRate: 44100, maxBlockSize: 512);
if (!ready)
{
Console.WriteLine($"State: {plugin.State}"); // VstPluginState.Error
return;
}
// 3. Query plugin info (all async, all on plugin thread).
string name = await plugin.GetNameAsync();
string vendor = await plugin.GetVendorAsync();
Console.WriteLine($"Loaded: {name} by {vendor}");
// 4. Real-time audio — call ProcessAudio directly from your audio thread.
// It drains the UI→audio parameter queue before every block.
bool ok = plugin.ProcessAudio(inputs, outputs, numChannels: 2, numSamples: 512);
// 5. Change parameters from the UI thread — lock-free, applied on next audio block.
plugin.SetParameter(paramId: 0, value: 0.75);
plugin.SetTempo(bpm: 120.0);
plugin.SetTransportState(isPlaying: true);
// 6. Open the plugin editor on the UI thread (VST3 + OS requirement).
var editor = new VstEditorController(plugin);
await editor.OpenEditorAsync(name);VST3 works exactly as it always has — passing a .vst3 path to LoadPluginAsync is unchanged. On macOS the same call also accepts a .component path or an AudioUnit identifier, so both formats share one API.
AudioUnits cannot be found by walking directories: they live in the system's AudioComponent registry and are addressed by an identifier rather than a file path. That is what PluginScanner is for.
// Fast scan — reads the AudioComponent registry and VST3 bundle names.
// Nothing is loaded, so this returns in a fraction of a second.
IReadOnlyList<PluginDescriptor> found = await PluginScanner.ScanAsync();
foreach (var p in found.Where(p => p.Format == PluginFormat.AudioUnit))
Console.WriteLine($"{p.Name} — {p.Vendor} — {p.Identifier}");
// Load one — Identifier is a path for VST3, an "AudioUnit:..." token for AU.
await plugin.LoadPluginAsync(found[0]);
Console.WriteLine(await plugin.GetFormatAsync()); // PluginFormat.AudioUnit
// Channel counts are -1 after a fast scan; fill them in for one plugin on demand.
PluginDescriptor? full = await PluginScanner.ResolveAsync(found[0]);A fast scan skips channel counts because getting them means loading the plugin. If you really need them for the whole list up front, ask for ScanMode.Full — that loads every plugin and takes minutes, so report progress and cache the result:
var progress = new Progress<ScanProgress>(p =>
Console.WriteLine($"{p.Fraction:P0} — {p.CurrentItem}"));
await PluginScanner.ScanAsync(PluginFormat.All, ScanMode.Full, progress);
File.WriteAllText(cacheFile, PluginScanner.GetCacheXml());
PluginScanner.RestoreCache(File.ReadAllText(cacheFile));OwnVst3Wrapper.FindVst3Plugins() is untouched and still does the plain VST3 directory walk — use it when you only care about VST3.
Note: AUv2 plugins are loaded in-process, so an Intel-only AudioUnit cannot be hosted by an ARM64 process. Under the hardened runtime the host needs the
com.apple.security.cs.disable-library-validationentitlement, same as for third-party VST3 bundles.
UI Thread ──── async/await ────▶ Plugin Thread (Load, Init, GetParameter…)
UI Thread ──── SPSC queue ────▶ Audio Thread (SetParameter, SetTempo…)
Audio Thread ──── direct call ────▶ ProcessAudio (no marshalling, no allocation)
UI Thread ──── UI thread ────▶ VstEditorController (CreateEditor, CloseEditor)
All state transitions are atomic. plugin.State and plugin.IsReady can be read safely from any thread at any time.
| State | Meaning |
|---|---|
NotLoaded |
Initial / after Dispose |
Loaded |
LoadPluginAsync succeeded |
Ready |
InitializeAsync succeeded — audio processing possible |
Processing |
Inside ProcessAudio (audio thread) |
Error |
Fatal failure — replace the instance |
// In your audio callback:
if (!plugin.IsReady) return;
plugin.ProcessAudio(inputs, outputs, channels, samples);| Platform | Architecture | Formats | Window backend |
|---|---|---|---|
| Windows | x64, x86, ARM64 | VST3 | Win32 STA thread + message loop |
| macOS | x64, ARM64 | VST3, AudioUnit | Cocoa via GCD (dispatch_sync to main thread) |
| Linux | x64, ARM64 | VST3 | X11 dedicated event loop thread |
Native libraries are resolved automatically from runtimes/{rid}/native/ at runtime.
OwnAudioVST3/
├── OwnVST3Host/ # Main library (C#)
│ ├── ThreadedVst3Wrapper.cs # Primary API — thread-safe VST3 façade
│ ├── OwnVst3Wrapper.cs # Low-level native wrapper + platform detection
│ ├── OwnVST3Formats.cs # Format-aware extras (AU identifier / bundle sub-index)
│ ├── PluginDiscovery.cs # PluginScanner, PluginDescriptor, PluginFormat
│ ├── LockFreeQueue.cs # SPSC ring buffer (UI → audio thread)
│ └── NativeWindow/
│ ├── INativeWindow.cs # Platform-agnostic window interface
│ ├── NativeWindowWindows.cs # Win32 STA window with message loop
│ ├── NativeWindowMac.cs # Cocoa/GCD main-thread marshalling
│ ├── NativeWindowLinux.cs # X11 event loop thread
│ ├── NativeWindowFactory.cs # Runtime platform selector
│ └── VstEditorController.cs # Editor lifecycle manager
├── OwnVST3Juce/ # Native JUCE-based backend (C++)
│ ├── CMakeLists.txt # Cross-platform build definition
│ ├── include/ownvst3_exports.h # C ABI exported by the native library
│ └── src/
│ ├── PluginInstance.cpp/h # JUCE AudioPluginInstance host wrapper
│ ├── PluginScanner.cpp/h # Background VST3 + AudioUnit discovery
│ ├── EditorWindow.cpp/h # Native plugin editor window management
│ ├── Exports.cpp # P/Invoke entry points
│ ├── SpscQueue.h # Lock-free parameter queue
│ └── StringCache.cpp/h # Thread-safe string interning
├── runtimes/ # Pre-built native libraries per RID
│ ├── win-x64/native/ownvst3.dll
│ ├── win-x86/native/ownvst3.dll
│ ├── win-arm64/native/ownvst3.dll
│ ├── osx-arm64/native/libownvst3.dylib
│ ├── osx-x64/native/libownvst3.dylib
│ ├── linux-x64/native/libownvst3.so
│ └── linux-arm64/native/libownvst3.so
└── OwnVST3EditorDemo/ # Avalonia demo application
# Clone the repository
git clone https://github.com/ModernMube/OwnVST3Sharp.git
cd OwnVST3Sharp
# Build the library
dotnet build OwnVST3Host/OwnVST3Host.csproj --configuration Release
# Run the demo
dotnet run --project OwnVST3EditorDemoNative library: Pre-built binaries for all platforms are included under
runtimes/. To rebuild the C++ JUCE backend from source, seeOwnVST3Juce/CMakeLists.txt— JUCE 8+ and CMake 3.22+ are required.
Full developer guide with API reference, threading rules, and code examples:
Topics covered:
- Architecture overview and threading diagram
- Plugin state machine reference
- Audio processing patterns (allocation-free path, channel handling)
- Parameter control: reading on plugin thread vs. writing via SPSC queue
- Transport and tempo integration
- MIDI scheduling with
SampleOffset - Editor lifecycle with Avalonia examples
- Plugin discovery (VST3 directories, AudioUnit scanning) and platform detection
- Best practices and common pitfalls
If you find this project helpful, consider buying me a coffee!
MIT License — see LICENSE.txt for details.
- Native plugin hosting powered by JUCE
- UI rendering powered by Avalonia UI
- Issues and contributions: GitHub Issues
