Well then. It's been a long time since I last got cozy with the unmanaged side of the CLR, but all the interop I have been doing lately has made me cocky. This post is a write-up of my attempts to develop a VST plugin in C#.
VST stands for Virtual Studio Technology. It's a standard developed by Steinberg for producing audio software components. The idea is that a VST host application (Cubase, Logic Audio, ACID etc) acts as a software mixing desk, routing audio in and out of these plugins. A plugin can be an effect, such as reverb or chorus, which changes the sound fed into them in some way; or an instrument which produces sounds of its own.
Steinberg provides an SDK which developers can use to make plugins but it only provides a C++ API. This is understandable as plugins need to be fast and work in realtime, and any long pauses could cause glitches in an audio stream or throw timing off. In addition, C++ is the de facto standard for writing audio software. On the other hand, as we have seen with Managed DirectX, it is possible to write performance-sensitive software in managed code as long as an effort is made to reduce the impact of garbage-collection and other overhead.
VST plugins are loaded into the host application as native DLLs, so I needed some way to call out into managed code from that point. There are a few options, but I took the route of hosting the CLR and calling into managed code with COM interop: partly because it sounds cool, and partly because it was the first thing that worked.
I’m not going to explain how to use the VST SDK – there are plenty of tutorials out there (this is an excellent one). This post is about hosting the CLR and calling into managed code, and assumes you can stumble your way through C++ COM code.
Plugins inherit from a base class provided in the SDK called AudioEffectX. The constructor would seem to be an appropriate place to start up the CLR, because it is called before any performance-critical processing takes place. The code to do this is well-documented (here, for starters), but looks something like this:
ICorRuntimeHost *pRuntimeHost = NULL;
On its own, this won’t get us very far. We need some managed code to run! Getting a reference to the default AppDomain is easy enough:
IUnknown *punk = NULL;
_AppDomain *pDefaultDomain = NULL;
punk->QueryInterface(__uuidof(_AppDomain), (PVOID*) &pDefaultDomain);
The _AppDomain interface is, essentially, identical to the managed AppDomain class which means we use it to load assemblies, create objects, create new AppDomains and all that good stuff. My managed code is in an assembly called “ManagedVst”, and I want to create an object of type “PluginAdapter” in the “ManagedVst” namespace:
_ObjectHandle *pObjHandle = NULL;
pDefaultDomain->CreateInstance(_bstr_t("ManagedVst"), _bstr_t("ManagedVst.PluginAdapter"), &pObjHandle);
Now we need to do some tedious unwrapping of all the COM layers around this object. Eventually we end up with a pointer to an interface matching that of the managed class (I defined this interface explicitly, but you can get the framework to create one for you).
v.pdispVal->QueryInterface(__uuidof(IPluginAdapter), (void**) &pPluginAdapter);
(Now you see why I want to write this plugin in C#?) So, finally, we have a pathway to a custom managed object which we can use and abuse at will. Next time, I’ll show how to marshal calls between the VST host and the managed plugin.