This is a small user guide to get you started with NPlug.
For more information about developing a VST3 plugin, please visit the official documentation at https://steinbergmedia.github.io/vst3_dev_portal/pages/
NPlug is following the recommendation of the VST3 architecture and requires that you develop your plugin by separating 2 components
- The
AudioController
component is responsible to manipulate the data model of your VST3 and provides an optional UI. It allows the VST3 host to interact with your VST when automating your plugin parameters. It allows your UI to notify the VST3 host about parameter changes. All this interaction is running on the UI Thread. - The
AudioProcessor
component is responsible to generate the audio (whether this is an effect plugin or an instruments) based on the data model. The data model is synchronized through the VST3 host.
The controller and processor should not share data directly (no static variables between them!) but instead should notify their changes through the model. Both the controller and processor maintain their own instance of the model, and changes are synchronized through the VST3 host.
NPlug provides the infrastructure to automatically synchronize the data model between the controller and the processor.
The AudioController
and AudioProcessor
implement some of the core NPlug VST interfaces:
In this section, we will port the the C/C++ adelay sample from https://github.com/steinbergmedia/vst3_public_sdk/tree/master/samples/vst/adelay/source
This sample is available in NPlug.SimpleDelay
in the NPlug sample repository.
NOTICE
This example is developed for the Windows platform as it is for now the easiest platform to work with for developing VST3 in .NET with NPlug.
Create a .NET7+ class library:
dotnet new classlib -n NPlug.SimpleDelay
Add NPlug to your project:
cd NPlug.SimpleDelay
dotnet add package NPlug
Create a class SimpleDelayModel
that inherits from NPlug.AudioProcessorModel
and add an AudioParameter
called Delay
:
namespace NPlug.SimpleDelay;
public class SimpleDelayModel : AudioProcessorModel
{
public SimpleDelayModel() : base("NPlug.SimpleDelay")
{
AddByPassParameter();
Delay = AddParameter(new AudioParameter("Delay", units: "sec", defaultNormalizedValue: 1.0));
}
public AudioParameter Delay { get; }
}
Notice that because we are developing a VST3 Effect, we are required to add the ByPass
parameter.
Create a class SimpleDelayController
that inherits from AudioController<TModel>
and by replacing TModel
with SimpleDelayModel
we just created.
We are also creating a new Guid
static field associated with this controller.
namespace NPlug.SimpleDelay;
public class SimpleDelayController : AudioController<SimpleDelayModel>
{
public static readonly Guid ClassId = new("484e783b-5f47-42f4-a19f-96c1fd42fb45");
}
The controller is very simple here because this sample does not have any UI and NPlug provides support for automatically synchronizing the model with the host.
Create a class SimpleDelayProcessor
that inherits from AudioProcessor<TModel>
and by replacing TModel
with SimpleDelayModel
we just created.
The following code contains the full implementation of the processor. The AudioProcessor
has several methods that can be overridden to implement the behavior. Mainly you need to:
- Initialize your plugin by overriding the
Initialize
method. In this method, you should define your input/output. - Implement the
OnActivate
method to allocate your buffers required by your plugin. - Implement the
ProcessMain
method to process the audio buffers and apply the plugin effect.
You will notice that in the ProcessMain
method, we are accessing the Model.Delay
variable. The Model instance is automatically synchronized with the controller.
Compare to the C++ version you will notice that we don't have to handle the serialization/deserialization of the parameters defined for our model. This is handled automatically by the AudioProcessorModel
class. All parameters are efficiently serialized/deserialized as double
float.
Similar to the controller, the processor provides also it's own Guid
field to identify it (used by the plugin registration in the next section).
Notice that we are associating the processor with the controller by implementing the property ControllerClassId
and we return the Guid
of the controller defined previously.
namespace NPlug.SimpleDelay;
/// <summary>
/// Port of C/C++ adelay sample from https://github.com/steinbergmedia/vst3_public_sdk/tree/master/samples/vst/adelay/source
/// </summary>
public class SimpleDelayProcessor : AudioProcessor<SimpleDelayModel>
{
private float[] _bufferLeft;
private float[] _bufferRight;
private int _bufferPosition;
public static readonly Guid ClassId = new("7a130e07-004a-408d-a1d8-97b671f36ca1");
public SimpleDelayProcessor() : base(AudioSampleSizeSupport.Float32)
{
_bufferLeft = Array.Empty<float>();
_bufferRight = Array.Empty<float>();
}
public override Guid ControllerClassId => SimpleDelayController.ClassId;
protected override bool Initialize(AudioHostApplication host)
{
AddAudioInput("AudioInput", SpeakerArrangement.SpeakerStereo);
AddAudioOutput("AudioOutput", SpeakerArrangement.SpeakerStereo);
return true;
}
protected override void OnActivate(bool isActive)
{
if (isActive)
{
var delayInSamples = (int)(ProcessSetupData.SampleRate * sizeof(float) + 0.5);
_bufferLeft = GC.AllocateArray<float>(delayInSamples, true);
_bufferRight = GC.AllocateArray<float>(delayInSamples, true);
_bufferPosition = 0;
}
else
{
_bufferLeft = Array.Empty<float>();
_bufferRight = Array.Empty<float>();
_bufferPosition = 0;
}
}
protected override void ProcessMain(in AudioProcessData data)
{
var delayInSamples = Math.Max(1, (int)(ProcessSetupData.SampleRate * Model.Delay.NormalizedValue));
for (int channel = 0; channel < 2; channel++)
{
var inputChannel = data.Input[0].GetChannelSpanAsFloat32(ProcessSetupData, data, channel);
var outputChannel = data.Output[0].GetChannelSpanAsFloat32(ProcessSetupData, data, channel);
var sampleCount = data.SampleCount;
var buffer = channel == 0 ? _bufferLeft : _bufferRight;
var tempBufferPos = _bufferPosition;
for (int sample = 0; sample < sampleCount; sample++)
{
var tempSample = inputChannel[sample];
outputChannel[sample] = buffer[tempBufferPos];
buffer[tempBufferPos] = tempSample;
tempBufferPos++;
if (tempBufferPos >= delayInSamples)
{
tempBufferPos = 0;
}
}
}
_bufferPosition += data.SampleCount;
while (_bufferPosition >= delayInSamples)
{
_bufferPosition -= delayInSamples;
}
}
}
We need to create a static class SimpleDelayPlugin
that will have a module initializer method ExportThisPlugin()
and create the associated AudioPluginFactory
.
Notice that we are using the Guid
of both the controller and processor to define our plugin.
Then we simply need to export our plugin by setting the factory instance AudioPluginFactoryExporter.Instance
.
using System.Runtime.CompilerServices;
namespace NPlug.SimpleDelay;
public static class SimpleDelayPlugin
{
public static AudioPluginFactory GetFactory()
{
var factory = new AudioPluginFactory(new("NPlug", "https://github.com/xoofx/NPlug", "no_reply@nplug.org"));
factory.RegisterPlugin<SimpleDelayProcessor>(new(SimpleDelayProcessor.ClassId, "SimpleDelay", AudioProcessorCategory.Effect));
factory.RegisterPlugin<SimpleDelayController>(new(SimpleDelayController.ClassId, "SimpleDelay Controller"));
return factory;
}
[ModuleInitializer]
internal static void ExportThisPlugin()
{
AudioPluginFactoryExporter.Instance = GetFactory();
}
}
If you build a debug version of your plugin with dotnet build
you will get the following generated files in the bin/Debug/net7.0
folder:
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 05-Mar-23 12:06 454144 NPlug.dll
-a--- 05-Mar-23 12:06 161332 NPlug.pdb
-a--- 05-Mar-23 09:39 782 NPlug.SimpleDelay.deps.json
-a--- 05-Mar-23 12:06 8192 NPlug.SimpleDelay.dll
-a--- 05-Mar-23 12:06 12080 NPlug.SimpleDelay.pdb
-a--- 05-Mar-23 09:39 349 NPlug.SimpleDelay.runtimeconfig.json
-a--- 26-Feb-23 17:53 342016 NPlug.SimpleDelay.vst3
You then need to copy all these files to the official VST3 location for plugins on Windows: C:\Program Files\Common Files\VST3
You can load your favorite VST3 host. In our case, we are going to use Renoise.
NOTICE
You will notice several files for this plugin. This is because we are using a debug setup without NativeAOT. In practice, the file
NPlug.SimpleDelay.vst3
is actually a generic native proxy that is going to load the .NET runtime in your VST host and instantiate your plugin by loading theNPlug.SimpleDelay.dll
assembly.This setup is easier to work with when you are developing your plugin.
Once you are ready, you can compile your plugin with NativeAOT.
Once you want to publish your plugin, you can compile your plugin to native with NativeAOT:
dotnet publish -c Release -r win-x64 -p:PublishAot=true
This will generate the following file in this folder NPlug.SimpleDelay\bin\Release\net7.0\win-x64\publish\
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 05-Mar-23 12:16 121636 NPlug.pdb
-a--- 05-Mar-23 12:16 13291520 NPlug.SimpleDelay.pdb
-a--- 05-Mar-23 12:16 3072512 NPlug.SimpleDelay.vst3
You can copy these files to the same VST3 folder and try again with your favorite VST Host. The pdb
files are only necessary if you want to debug your plugin with Visual Studio debugger.
This is all! You have created your first VST3 native plugin with NPlug in C#!
In this section we are covering some more advanced topics when using NPlug.
The following table shows the mapping between the VST3 interface and NPlug.
VST3 Interface | NPlug Interface |
---|---|
IAudioPresentationLatency |
IAudioProcessor |
IAudioProcessor |
IAudioProcessor |
IAutomationState |
IAudioControllerAutomationState |
IComponent |
IAudioProcessor |
IConnectionPoint |
IAudioConnectionPoint |
IContextMenuTarget |
System.Delegate |
IEditController |
IAudioController |
IEditController2 |
IAudioControllerExtended |
IEditControllerHostEditing |
IAudioControllerHostEditing |
IInfoListener |
IAudioControllerInfoListener |
IInterAppAudioPresetManager |
IAudioControllerInterAppAudioPresetManager |
IKeyswitchController |
IAudioControllerKeySwitch |
IMidiLearn |
IAudioControllerMidiLearn |
IMidiMapping |
IAudioControllerMidiMapping |
INoteExpressionController |
IAudioControllerNoteExpression |
INoteExpressionPhysicalUIMapping |
IAudioControllerNoteExpressionPhysicalUIMapping |
IParameterFinder |
IAudioPluginView |
IParameterFunctionName |
IAudioControllerParameterFunctionName |
IPluginBase |
IAudioPluginComponent |
IPluginFactory |
IAudioPluginFactory |
IPluginFactory2 |
IAudioPluginFactory |
IPluginFactory3 |
IAudioPluginFactory |
IPlugView |
IAudioPluginView |
IPlugViewContentScaleSupport |
IAudioPluginView |
IPrefetchableSupport |
IAudioProcessorPrefetchable |
IProcessContextRequirements |
IAudioProcessor |
IProgramListData |
IAudioProcessorProgramListData |
ITestPlugProvider |
IAudioTestProvider |
ITestPlugProvider2 |
IAudioTestProvider |
IUnitData |
IAudioProcessorUnitData |
IUnitInfo |
IAudioControllerUnitInfo |
IXmlRepresentationController |
IAudioControllerXmlRepresentation |
By default, the class AudioParameter
provides a simple float parameter.
You might want to use sub classes of this AudioParameter
to manipulate other types:
- The
AudioBoolParameter
allows to manipulate a boolean parameter. The value0.0f
will be mapped tofalse
while1.0f
will be mapped totrue
. - The
AudioRangeParameter
allows to manipulate a range of float with a min and max value and potential steps or continuous. - The
AudioStringListParameter
allows to display a list of string for a parameter. Behind the scene, the selected string is automatically mapped to a float ranging from 0.0f to 1.0f.
NPlug does not provide yet a sample with a UI for the main reason that I haven't found yet a simple UI framework that is lightweight, simple to setup and compatible with NativeAOT.
Creating a UI requires to implement the IAudioPluginView
interface and to override the AudioController.CreateView()
protected override IAudioPluginView? CreateView()
{
return new MyAudioPluginUI();
}
NPlug allows to trace all the interactions (call-in and call-out) between the host and your plugin.
First, you need to add set the property NPlugInteropTracer
in your C# project:
<PropertyGroup>
<NPlugInteropTracer>true</NPlugInteropTracer>
</PropertyGroup>
In your Plugin factory, setup the InteropTracer
by logging all calls to a temporary NPlug*.log
files in the temp folder:
InteropHelper.Tracer = new TempFileInteropTracer();
NPlug provides a package to validate your plugin. This can be used as part of your tests to make sure that your plugin is working.
The validation is using the validator provided by the VST3 SDK.
In order to use the validator, you need to install the package dotnet package add NPlug.Validator
.
In C#, you can directly call the validator on your AudioPluginFactory
instance. Let's take the example of the SimpleDelayPlugin
that we used in the getting started section:
var factory = SimpleDelayPlugin.GetFactory();
// Call the validator on the plugin
var result = AudioPluginValidator.Validate(factory.Export, Console.Out, Console.Error);
if (!result) {
// TODO: handle error
}
You can see the output of the validation for the SimpleDelayPlugin
here.
The project NPlug.Tests
in this repository is leveraging the validator to validate the plugins from the samples and the output is verified with a snapshot via Verify.
You can also validate a native plugin by passing the path to the vst3 plugin (on Windows). For other platforms, it would require to setup the plugin structure correctly (see issue #1)