Skip to content

Repository files navigation

OwnAudioVST3

OwnAudio Logo

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.

NuGet License: MIT


Features

  • 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 .component bundles 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-x64 and more

Installation

dotnet add package OwnVst3Host

Quick Start

using 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);

Audio Units (macOS)

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-validation entitlement, same as for third-party VST3 bundles.


Threading Model

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 Support

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.


Project Structure

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

Building from Source

# 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 OwnVST3EditorDemo

Native library: Pre-built binaries for all platforms are included under runtimes/. To rebuild the C++ JUCE backend from source, see OwnVST3Juce/CMakeLists.txt — JUCE 8+ and CMake 3.22+ are required.


Documentation

Full developer guide with API reference, threading rules, and code examples:

OwnVST3Host/README.md

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

Support My Work

If you find this project helpful, consider buying me a coffee!

Buy Me A Coffee


License

MIT License — see LICENSE.txt for details.

Acknowledgments

About

OwnVst3 CSharp wrapper easy use plugin host

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages