OwnAudioVst 1.5.20

There is a newer version of this package available.
See the version list below for details.
dotnet add package OwnAudioVst --version 1.5.20
                    
NuGet\Install-Package OwnAudioVst -Version 1.5.20
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OwnAudioVst" Version="1.5.20" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OwnAudioVst" Version="1.5.20" />
                    
Directory.Packages.props
<PackageReference Include="OwnAudioVst" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OwnAudioVst --version 1.5.20
                    
#r "nuget: OwnAudioVst, 1.5.20"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OwnAudioVst@1.5.20
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OwnAudioVst&version=1.5.20
                    
Install as a Cake Addin
#tool nuget:?package=OwnAudioVst&version=1.5.20
                    
Install as a Cake Tool

OwnVST3Host — Developer Guide

A thread-safe, cross-platform C# library for hosting VST3 plugins. Designed to integrate cleanly into audio applications where the UI thread, the audio thread, and the plugin's native runtime must never interfere with each other.


Table of Contents

  1. Architecture Overview
  2. Threading Model
  3. Plugin State Machine
  4. Quick Start
  5. Loading and Initializing a Plugin
  6. Audio Processing
  7. Parameter Control
  8. Transport and Tempo
  9. MIDI
  10. Plugin Editor UI
  11. Plugin Discovery
  12. Platform Support
  13. API Reference
  14. Best Practices
  15. Common Pitfalls

Architecture Overview

The library exposes two layers:

Class Role
ThreadedVst3Wrapper Primary API. Thread-safe façade. All native VST3 operations run on a dedicated plugin thread. Audio callbacks run directly on the audio thread without marshalling.
VstEditorController Manages the native OS window and the plugin's visual editor. Editor lifecycle (open/close) must stay on the UI thread.
OwnVst3Wrapper Low-level native wrapper. Used internally. Exposes platform detection and plugin discovery as static helpers. Do not call from multiple threads directly.
┌──────────────┐    async/await     ┌─────────────────────┐    native calls    ┌─────────────────┐
│   UI Thread  │ ────────────────▶  │ ThreadedVst3Wrapper │ ────────────────▶  │  Plugin Thread  │
│              │                    │                     │                    │ (LoadPlugin,    │
│ OpenEditor() │ ─── UI thread ───▶ │  VstEditorController│                    │  Initialize,    │
└──────────────┘                    └─────────────────────┘                    │  GetParameter…) │
                                             │                                 └─────────────────┘
┌──────────────┐   ProcessAudio()            │  SPSC queue
│ Audio Thread │ ──── direct ──────────────▶ │  (SetParameter,
│              │                             │   SetTempo…)
└──────────────┘                             ▼
                                    ┌─────────────────┐
                                    │   Audio Thread  │
                                    │  (DrainQueue,   │
                                    │   ProcessAudio) │
                                    └─────────────────┘

Threading Model

This is the most important concept to understand before integrating the library.

Four threads, four rules

Thread Responsibility What you call here
Plugin thread All native VST3 operations except audio LoadPluginAsync, InitializeAsync, GetNameAsync, GetParameterAsync, …
Audio thread Real-time audio and MIDI processing ProcessAudio, SendMidiEvent, ProcessMidi
UI thread Editor window lifecycle (VST3 + OS requirement) OpenEditorAsync, CloseEditor
Any thread Lock-free state changes and state queries SetParameter, SetTempo, SetTransportState, State, IsReady

Key rule

Never call ProcessAudio from the UI or plugin thread. Never call LoadPluginAsync or InitializeAsync from the audio thread.

The library enforces this through the state machine — ProcessAudio only executes when the plugin is in the Ready state, and it atomically transitions to Processing for the duration of each callback.


Plugin State Machine

ThreadedVst3Wrapper.State is safe to read from any thread at any time. It uses Volatile.Read / Interlocked internally — no locks required.

NotLoaded ──▶ Loaded ──▶ Ready ◀──▶ Processing
    │             │          │
    └─────────────┴──────────┴──▶ Error
    ▲
  Dispose()
State Meaning
NotLoaded No plugin loaded. Initial state and state after Dispose().
Loaded LoadPluginAsync succeeded. Plugin factory created, not yet initialized for audio.
Ready InitializeAsync succeeded. Plugin is ready to process audio.
Processing ProcessAudio is currently executing on the audio thread.
Error LoadPluginAsync or InitializeAsync returned false. Create a new instance to retry.
// Reading state from any thread — safe and allocation-free
VstPluginState state = plugin.State;

// Convenience shortcut: Ready || Processing
bool canProcess = plugin.IsReady;

// React to state changes in the UI
if (plugin.State == VstPluginState.Error)
{
    ShowError("Plugin failed to initialize.");
    return;
}

Quick Start

using OwnVST3Host;
using OwnVST3Host.NativeWindow;

// 1. Create the wrapper — starts the plugin thread immediately.
await using var plugin = new ThreadedVst3Wrapper();

// 2. Load and initialize — runs on the plugin thread, does not block the UI.
bool loaded = await plugin.LoadPluginAsync("/Library/Audio/Plug-Ins/VST3/MyPlugin.vst3");
if (!loaded) return;

bool initialized = await plugin.InitializeAsync(sampleRate: 44100, maxBlockSize: 512);
if (!initialized) return;

// 3. Query info — all async, all on plugin thread.
string name    = await plugin.GetNameAsync();
string vendor  = await plugin.GetVendorAsync();
bool isEffect  = await plugin.GetIsEffectAsync();

Console.WriteLine($"{name} by {vendor} — {(isEffect ? "Effect" : "Instrument")}");

// 4. Hand off to the audio engine. From here on, the audio thread calls ProcessAudio().
audioEngine.Start(plugin);

Loading and Initializing a Plugin

Basic load sequence

var plugin = new ThreadedVst3Wrapper();

// LoadPlugin runs on the plugin thread. Await it — the UI thread is never blocked.
bool loaded = await plugin.LoadPluginAsync(pluginPath);

if (!loaded || plugin.State == VstPluginState.Error)
{
    Console.WriteLine("Failed to load plugin.");
    plugin.Dispose();
    return;
}

// State is now Loaded. Initialize the audio engine.
bool ready = await plugin.InitializeAsync(sampleRate: 48000, maxBlockSize: 256);

if (!ready || plugin.State != VstPluginState.Ready)
{
    Console.WriteLine("Failed to initialize plugin.");
    plugin.Dispose();
    return;
}

// Safe to hand off to the audio thread.

Loading a specific native library

If you need to load from a custom path:

string nativePath = "/opt/myapp/runtimes/osx-arm64/native/libownvst3.dylib";
var plugin = new ThreadedVst3Wrapper(nativePath);

Re-loading a different plugin

ThreadedVst3Wrapper is single-use: one instance = one plugin. To load a different plugin, dispose and create a new instance.

// Wrong — LoadPlugin on an already-loaded wrapper is not defined behavior.
// Correct:
await currentPlugin.DisposeAsync(); // or plugin.Dispose()
currentPlugin = new ThreadedVst3Wrapper();
await currentPlugin.LoadPluginAsync(newPluginPath);

Audio Processing

ProcessAudio is the only method called directly from the audio thread. It is designed to be allocation-free and lock-free after the first call.

Calling from the audio thread

// In your audio engine callback — this runs on the audio thread.
void AudioCallback(float[][] inputs, float[][] outputs, int numSamples)
{
    // Guard: plugin might not be ready yet (loading in background).
    if (!plugin.IsReady)
    {
        // Pass-through silence
        foreach (var ch in outputs) Array.Clear(ch, 0, numSamples);
        return;
    }

    // ProcessAudio:
    //   1. Atomically transitions Ready → Processing.
    //   2. Drains the UI→Audio SPSC queue (SetParameter, SetTempo, …).
    //   3. Calls the native plugin's processing function.
    //   4. Transitions back to Ready.
    bool success = plugin.ProcessAudio(inputs, outputs, numChannels: 2, numSamples);

    if (!success)
    {
        // Plugin reported an error or was in wrong state.
        foreach (var ch in outputs) Array.Clear(ch, 0, numSamples);
    }
}

What happens inside ProcessAudio

Audio thread enters ProcessAudio()
    │
    ├─ Check _disposed (volatile read) → return false if disposed
    ├─ CAS: Ready → Processing         → return false if not Ready
    ├─ Check _disposed again           → guard against concurrent Dispose
    ├─ DrainStateQueue()               → apply SetParameter / SetTempo / … from UI
    ├─ _inner.ProcessAudio(...)        → native plugin call (allocation-free)
    └─ CAS: Processing → Ready         → restore state

Handling channel counts

The plugin declares its own channel layout after Initialize. The wrapper automatically clamps the call to the plugin's declared channel count and passes through any extra channels unchanged:

// If your audio engine uses 8 channels but the plugin only handles stereo,
// channels 0-1 go through the plugin and channels 2-7 are passed through.
plugin.ProcessAudio(inputs, outputs, numChannels: 8, numSamples: 512);

Parameter Control

Reading parameters (plugin thread)

Parameter reads must go through the plugin thread because they call native code:

// Get the full parameter list once after loading.
List<VST3Parameter> parameters = await plugin.GetAllParametersAsync();

foreach (var p in parameters)
{
    Console.WriteLine($"[{p.Id}] {p.Name}: {p.CurrentValue:F3} (range {p.MinValue}–{p.MaxValue})");
}

// Read a single parameter value.
double gain = await plugin.GetParameterAsync(paramId: 0);

Setting parameters (any thread, lock-free)

SetParameter enqueues a change into a lock-free SPSC queue. The audio thread drains this queue at the start of every ProcessAudio call, applying the change with ~11 ms latency at 44100/512.

// Safe to call from the UI thread, a timer callback, or anywhere else.
// Returns immediately — does not wait for the audio thread.
plugin.SetParameter(paramId: 0, value: 0.75);
plugin.SetParameter(paramId: 1, value: 0.5);

Note: The queue holds 512 pending changes. For typical UI interaction (sliders, knobs) this is never an issue. If the queue is ever full, the change is logged and dropped — this indicates a bug in the caller.

Automating parameters at audio rate

For sample-accurate automation, send changes before each ProcessAudio call in the audio callback:

void AudioCallback(float[][] inputs, float[][] outputs, int numSamples)
{
    // Apply automation value for this block.
    plugin.SetParameter(gainParamId, automationEnvelope.NextValue());

    // ProcessAudio drains the queue first, so the value above is applied
    // before the native plugin processes this block.
    plugin.ProcessAudio(inputs, outputs, 2, numSamples);
}

Transport and Tempo

These are also lock-free SPSC queue operations — safe from any thread, applied on the audio thread.

// Set BPM (forwarded to the plugin via ProcessContext on every block).
plugin.SetTempo(bpm: 120.0);

// Start / stop transport.
plugin.SetTransportState(isPlaying: true);
plugin.SetTransportState(isPlaying: false);

// Reset playhead to zero (e.g. on Stop).
plugin.ResetTransportPosition();

Typical transport integration with a play/stop UI:

playButton.Click += (_, _) =>
{
    plugin.SetTransportState(true);
    plugin.SetTempo(currentBpm);
};

stopButton.Click += (_, _) =>
{
    plugin.SetTransportState(false);
    plugin.ResetTransportPosition();
};

MIDI

MIDI methods are called directly from the audio thread — they do not go through the SPSC queue and have zero latency.

// In your audio callback:

// Note On — Middle C, velocity 100
plugin.SendMidiEvent(status: 0x90, data1: 60, data2: 100);

// Note Off
plugin.SendMidiEvent(status: 0x80, data1: 60, data2: 0);

// Control Change — Sustain pedal on
plugin.SendMidiEvent(status: 0xB0, data1: 64, data2: 127);

// Program Change — bank 0, patch 5
plugin.SendMidiEvent(status: 0xC0, data1: 5, data2: 0);

Sending multiple events in one block

var events = new MidiEvent[]
{
    new() { Status = 0x90, Data1 = 60, Data2 = 80, SampleOffset = 0   },
    new() { Status = 0x90, Data1 = 64, Data2 = 80, SampleOffset = 128 },
    new() { Status = 0x90, Data1 = 67, Data2 = 80, SampleOffset = 256 },
};

plugin.ProcessMidi(events);

SampleOffset places each event at a precise sample position within the current block, enabling sample-accurate MIDI scheduling.

MIDI-only plugins

Some plugins (arpeggiators, chord generators, MIDI effects) accept MIDI but produce no audio output.

bool isMidiOnly = await plugin.GetIsMidiOnlyAsync();

if (isMidiOnly)
{
    // No audio processing needed.
    // Just drive the plugin with MIDI from the audio callback.
    plugin.SendMidiEvent(0x90, 60, 100);
}

Plugin Editor UI

The editor must be opened and closed on the UI thread. This is a hard requirement imposed by VST3 and enforced by macOS (Cocoa) and Windows (STA COM).

Opening the editor (async, preferred)

// Runs entirely on the UI thread.
// GetEditorSize() is fetched asynchronously on the plugin thread;
// the native window is created and CreateEditor() is called on the UI thread.
var editorController = new VstEditorController(plugin);
await editorController.OpenEditorAsync("My Plugin");

Opening the editor (synchronous, for non-async contexts)

var editorController = new VstEditorController(plugin);
editorController.OpenEditor("My Plugin");

Closing the editor

if (editorController.IsOpen)
    editorController.CloseEditor();

Full editor lifecycle example (Avalonia)

private VstEditorController? _editor;

private async void OnOpenEditorClick(object? sender, RoutedEventArgs e)
{
    if (_editor?.IsOpen == true)
    {
        _editor.CloseEditor();
        openButton.Content = "Open Editor";
        return;
    }

    if (_editor == null)
        _editor = new VstEditorController(_plugin);

    string name = await _plugin.GetNameAsync();
    await _editor.OpenEditorAsync(name);

    openButton.Content = "Close Editor";
}

protected override void OnClosed(EventArgs e)
{
    _editor?.CloseEditor();
    _editor?.Dispose();
    _plugin?.Dispose();
    base.OnClosed(e);
}

How the editor thread model works

UI Thread                  Plugin Thread            Native Window Thread (Win) / Main Thread (Mac)
    │                           │                              │
    ├─ await GetEditorSizeAsync()─▶ GetEditorSize()            │
    │◀──────────────── size ────────┤                          │
    ├─ NativeWindowFactory.Create() ──────────────────────────▶│
    │                               Open(title, w, h)          │  (Win: STA message loop)
    │◀───────────────────────────────────── handle ────────────┤  (Mac: dispatch_sync to main)
    ├─ CreateEditor(handle)  ← runs on UI thread (VST3 requirement)
    │
    └─ StartIdleThread() → ProcessIdle at 50 Hz (via BeginInvoke on window thread)

Plugin Discovery

// List all standard VST3 directories for the current OS.
string[] dirs = OwnVst3Wrapper.GetDefaultVst3Directories();

// Scan and return all found .vst3 bundles.
List<string> plugins = OwnVst3Wrapper.FindVst3Plugins();

// Scan specific directories only.
List<string> plugins = OwnVst3Wrapper.FindVst3Plugins(
    new[] { "/usr/lib/vst3", "~/.vst3" });

// Human-readable diagnostic output (useful for debugging).
Console.WriteLine(OwnVst3Wrapper.GetVst3DirectoriesInfo());
// Output:
//   Platform: osx-arm64
//   VST3 Plugin Directories:
//     [OK] /Library/Audio/Plug-Ins/VST3
//     [OK] /Users/yourname/Library/Audio/Plug-Ins/VST3

Runtime / architecture detection

string rid  = OwnVst3Wrapper.GetRuntimeIdentifier(); // "osx-arm64", "win-x64", "linux-x64"
string lib  = OwnVst3Wrapper.GetNativeLibraryName();  // "libownvst3.dylib", "ownvst3.dll", …
string path = OwnVst3Wrapper.GetNativeLibraryPath();  // full path to the loaded native library

Platform Support

Platform Architecture Window API Notes
Windows x64, x86 Win32 (STA thread) Full editor and audio support
macOS x64, ARM64 Cocoa (main thread via GCD) Requires [STAThread] or Avalonia dispatcher
Linux x64 X11 (dedicated event thread) Requires running X server; Wayland via XWayland

Native library search order

The library is searched in this order at runtime:

  1. runtimes/{rid}/native/ relative to the assembly location
  2. runtimes/{rid}/native/ relative to the current directory
  3. Assembly directory (flat layout)
  4. Current directory (flat layout)

API Reference

ThreadedVst3Wrapper

State
Member Thread Description
State Any Current VstPluginState. Volatile read, zero allocation.
IsReady Any true when State is Ready or Processing.
IsEditorOpen Any true when the plugin's editor view is attached.
Lifecycle (plugin thread)
Method Returns Description
LoadPluginAsync(path) Task<bool> Load the plugin binary and create the factory. Sets state to Loaded or Error.
InitializeAsync(sampleRate, maxBlockSize) Task<bool> Start the audio engine. Sets state to Ready or Error.
Dispose() Stop the plugin thread, free all native resources.
Info queries (plugin thread)
Method Returns
GetNameAsync() Task<string>
GetVendorAsync() Task<string>
GetVersionAsync() Task<string?>
GetIsInstrumentAsync() Task<bool>
GetIsEffectAsync() Task<bool>
GetIsMidiOnlyAsync() Task<bool>
GetPluginInfoAsync() Task<string>
GetParameterCountAsync() Task<int>
GetAllParametersAsync() Task<List<VST3Parameter>>
GetParameterAtAsync(index) Task<VST3Parameter>
GetParameterAsync(paramId) Task<double>
GetEditorSizeAsync() Task<EditorSize?>
Lock-free state changes (any thread → audio thread)
Method Latency
SetParameter(paramId, value) ~1 block (~11 ms at 44100/512)
SetTempo(bpm) ~1 block
SetTransportState(isPlaying) ~1 block
ResetTransportPosition() ~1 block
Audio thread direct calls
Method Returns Notes
ProcessAudio(inputs, outputs, numChannels, numSamples) bool Only valid when IsReady. Transitions state Ready↔Processing.
SendMidiEvent(status, data1, data2) bool Zero latency, no queue.
ProcessMidi(MidiEvent[]) bool Zero latency, no queue.

VstEditorController

Member Thread Description
OpenEditorAsync(title) UI Preferred. Async size fetch + sync editor attach.
OpenEditor(title) UI Synchronous variant.
CloseEditor() UI Detach editor and close native window.
IsOpen Any true if the native window is open.
Dispose() UI Closes editor and releases all OS window resources.

VstPluginState enum

public enum VstPluginState
{
    NotLoaded,   // Initial / after Dispose
    Loaded,      // LoadPlugin succeeded, not yet initialized
    Ready,       // Initialize succeeded, audio processing possible
    Processing,  // Inside ProcessAudio (set/cleared by audio thread)
    Error        // LoadPlugin or Initialize failed — replace the instance
}

Best Practices

1. Always check state before processing

// In your audio callback:
if (!plugin.IsReady) return;
plugin.ProcessAudio(inputs, outputs, channels, samples);

2. Stop audio before Dispose

The Dispose method waits up to 100 ms for a running ProcessAudio call to complete before freeing native memory. To guarantee a clean shutdown, always stop the audio engine before calling Dispose:

audioEngine.Stop();              // ensures no more ProcessAudio calls
await Task.Delay(50);            // optional: let any in-flight callback finish
plugin.Dispose();

3. One instance per plugin

ThreadedVst3Wrapper is not reusable. Create a new instance for each plugin, or when reloading the same plugin with different settings:

plugin?.Dispose();
plugin = new ThreadedVst3Wrapper();
await plugin.LoadPluginAsync(newPath);
await plugin.InitializeAsync(sampleRate, blockSize);

4. Use await using for automatic cleanup

await using var plugin = new ThreadedVst3Wrapper();
// ... plugin is disposed automatically when the scope exits

5. Check Error state after every async operation

await plugin.LoadPluginAsync(path);
if (plugin.State == VstPluginState.Error) { /* handle */ return; }

await plugin.InitializeAsync(44100, 512);
if (plugin.State == VstPluginState.Error) { /* handle */ return; }

6. Match MIDI calls to the audio block

Send MIDI events inside the audio callback, not from the UI thread. The SendMidiEvent / ProcessMidi methods are audio-thread-only:

// Wrong — calling from UI thread is not safe:
button.Click += (_, _) => plugin.SendMidiEvent(0x90, 60, 100);

// Correct — enqueue the note, send it in the audio callback:
button.Click += (_, _) => pendingNotes.Enqueue(new MidiEvent(0x90, 60, 100));

void AudioCallback(...)
{
    while (pendingNotes.TryDequeue(out var evt))
        plugin.SendMidiEvent(evt.Status, evt.Data1, evt.Data2);
    plugin.ProcessAudio(...);
}

Common Pitfalls

Calling ProcessAudio before InitializeAsync

ProcessAudio returns false silently when the plugin is not in Ready state. Always await InitializeAsync before starting the audio engine.

Calling plugin thread methods from the audio callback

// WRONG — GetParameterAsync posts to the plugin thread and blocks!
void AudioCallback(...)
{
    double val = await plugin.GetParameterAsync(0); // ← blocks audio thread!
}

// CORRECT — read parameters asynchronously from the UI and cache the result.
double cachedGain = 1.0;
// On plugin thread (before audio starts):
cachedGain = await plugin.GetParameterAsync(0);
// In audio callback:
ApplyGain(outputs, cachedGain);

Opening the editor from a background thread

// WRONG — CreateEditor must run on the UI thread (VST3 + OS requirement).
Task.Run(() => editorController.OpenEditor("Plugin")); // ← undefined behavior

// CORRECT — dispatch to the UI thread.
Dispatcher.UIThread.InvokeAsync(() => editorController.OpenEditor("Plugin"));

Disposing while the audio thread is still running

// WRONG — Dispose during active audio processing risks a crash.
plugin.Dispose(); // audio thread might still be inside ProcessAudio!

// CORRECT — stop audio first.
audioEngine.Stop();
plugin.Dispose();

Expecting SetParameter changes to be instant

SetParameter targets the audio thread via the SPSC queue. The change takes effect on the next audio block (~11 ms at 44100/512). For UI feedback, maintain your own cached value:

private double _gainValue;

void OnSliderChanged(double value)
{
    _gainValue = value;          // UI cache, instant
    plugin.SetParameter(0, value); // audio queue, ~11 ms
    gainLabel.Text = $"{value:F2}"; // update label from UI cache
}
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on OwnAudioVst:

Package Downloads
OwnAudioSharp

Complete cross-platform audio engine for .NET desktop apps (Windows, Linux, macOS), built on a native Rust core. Playback, recording, multi-track mixing, effects, VST3 hosting and MIDI, plus the analysis features (chord detection, note transcription, matchering) and the Avalonia waveform display. AOT-compatible and trimmable.

OwnAudioSharp.Basic

Minimal cross-platform audio engine for .NET desktop apps (Windows, Linux, macOS), built on a native Rust core. Audio in and out only: playback, recording, multi-track mixing, effects and VST3 hosting. Everything that could be left out is left out - no analysis features, no ONNX models, no UI dependency - so it stays a fraction of the full package size. AOT-compatible and trimmable.

OwnAudioSharp.Mobile

Cross-platform audio engine for .NET mobile apps (Android, iOS), built on a native Rust core. Playback, recording, multi-track mixing, effects, VST3 hosting, matchering and the Avalonia waveform display, without the ONNX-based analysis features (no chord detection, no note transcription). AOT-compatible and trimmable.

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on OwnAudioVst:

Repository Stars
ModernMube/OwnAudioSharp
Ownaudio is a cross-platform C# audio library that works across platforms without external dependencies.
Version Downloads Last Updated
1.7.0 45 9/8/2026
1.6.8 154 8/29/2026
1.6.7 2,911 7/5/2026
1.6.4 906 6/14/2026
1.6.3 773 5/30/2026
1.6.0 169 5/26/2026
1.5.21 123 5/23/2026
1.5.20 242 5/10/2026
1.5.7 149 5/1/2026
1.4.6 168 4/21/2026
1.4.1 348 4/8/2026
1.4.0 462 2/15/2026
1.3.2 138 1/31/2026
1.3.1 142 1/30/2026
Loading failed