Introduction
This article demonstrates a means of communicating with Audio devices which support Steinberg’s ASIO drivers from within .NET. This allows low level and low latency communication with soundcards and might prove useful for those interested in developing audio applications – software synthesisers, recording applications, FX units and such things. Please note that the source code supplied is not complete; you will need to download the ASIO SDK from Steinberg in order to build and run it. Licensing restrictions prevent me from distributing it here.
Because the library on its own is somewhat dull, I’ve included a small console application to demonstrate its use – the slurring idiot application. With this, a microphone and a pair of headphones you should be able to turn yourself into a low latency slurring idiot with minimum effort. A bit like a bucket of Belgian lager without the hangover.
Background
Being a nerd, I’m currently revisiting a project I’ve done little bits on over the years. It’s a music synthesiser. Many years ago I bought a lot of studio grade keyboards and rack units in the hope of putting them to good use making tunes but as it turned out I was a poor musician. None-the-less, I still loved these machines and the rich textured sounds they could produce.
In those days, these bits of kit had dedicated DSP chips in them to achieve their results, but the years have seen computers Moore’s Law themselves out and there has been a general shift away from hardware sound modules to software counterparts. In particular there are many commercial VST plug-ins available nowadays which are fully featured synthesisers and samplers which integrate with sequencers.
It’s not in my brief to make anything VST compliant, perhaps if I were aiming for commercial success I would go down this route. Instead I’m interested in producing a stand alone synthesiser and to so using .NET which for me is the best software development environment available at present.
DirectX vs. ASIO
If you want to develop an audio application, the first thing you’re going to need is a way of getting the sound in and out of it – your soundcard. There are varying ways to do this, and in my first cut I used DirectSound. This works perfectly – to an extent.
An audio stream is simply a flow of numbers or ‘samples’ which represent sound pressure. The more samples you use a second the higher the ‘sample rate’ and the better the quality, in particular in the higher frequency components of the sound. CDs sample at 44.1KHz, DVDs at 48KHz. So, if we want CD quality audio, we need to be prepared to make 44100 reads and writes to the soundcard a second. For stereo, we need to do it twice.
In practice, it is impossible to interrupt the processor that many times a second so a system of buffering is used, this helps keep the work to a minimum and helps with things like IO when reading from disc or network. A more reasonable approach is to interrupt the processor 100 times a second and get it to spit out a buffer of 441 samples each time. The bigger the buffer, the greater the efficiency but the greater the latency. With a 441 sample buffer, you get a minimum latency of 1/100th of a second and a maximum or 1/50th. This is the minimum time it’s possible to hear a note after you strike a note on a keyboard.
There is always a compromise in deciding what buffer size to use – large gives you great performance but a delay. Small eats up at performance but minimises this delay. Adding a 1/100th second delay to a sound will perceptibly alter it too, as if you were in a big room.
And this is the problem with DirectX, the buffer sizes it uses are just too big to make an effective real-time audio program, and this is why audio software boffins Steinberg went about creating a new specification for low-latency audio drivers. They created a standard called ASIO which is designed to give low level access to your audio hardware using small buffers which minimises latency. If you’re developing a serious real-time audio application, you’re going to need ASIO.
My soundcard doesn’t support ASIO!
If you’re posh like me you may have a posh soundcard which provides ASIO support. Fear not if that’s not the case, Michael Tippach has come to the rescue. He has developed an ASIO driver which works seemingly with just about every sound card out there. I don’t know how he’s done it but clearly he’s a clever chap. You can download it from his website here.
The ASIO specification
ASIO is free, sort of. Steinberg being good natured types have published the standard and anyone is free to use it. What they don’t like is you distributing it or using it commercially without acknowledging them, and it is for this reason that you’ll need to download the SDK direct from them rather than me distributing the bits we need. More on that below.
An ASIO driver is not a driver in the sense you’re probably used to. E.g. Some kernel mode nasty binary thing which sits at the bottom of the operating system. An ASIO driver is a COM object which talks to your soundcard. How it does this varies from card to card and I don't know the exact mechanics.
In theory then, to start producing some wicked sound in .NET a bit of COM Interop should do the trick – instantiate the COM object and call methods via its interface. Unfortunately as it turns things aren’t that simple. Steinberg made a couple of interesting choices in the Windows implementation of their standard and the big one is that the CLSID of the object and the IID of its primary interface are always the same.
This means we don’t know the IID until we know the object, so simple COM Interop which expects us to know this IID in advance won’t work. We’re going to need a bit of mixed managed/unmanaged C++ to do this.
How it works
I'm not going to go into too much detail about this because it's boring. Have a look at the code if you want to know exactly how it works, but in summary...
The first thing to do is decide on which ASIO driver to use should there be more than one installed on your system. The drivers make themselves known by adding entries to the registry (HKEY_LOCAL_MACHINE\SOFTWARE\ASIO
). Each driver registers its name and the CLSID of the COM object here. We need to iterate through this key to get each driver.
Once we've decided upon which driver we want to use, we instantiate it (CoCreateInstance
) and get a pointer to its interface, we can then start calling methods and register a load of callbacks to respond to events. We ask how many input and output channels it has.
We then create a managed Channel
to represent each of these, each containing a buffer which our managed calling application can update.
ASIO supports various different sample formats but this implementation only uses one, 32bit signed integers. I wasn't too keen on exposing this to our calling app, so instead we expose doubles which have a permissible range of -1.0 to 1.0. When we copy samples between buffers we do a conversion back and forth. Each Channel then makes available an array of doubles with a capacity equal to the desired buffer size.
When we ask the driver to start, it will start playing the output channels and capturing the input channels, firing a callback each time a buffer update is required. In our assembly we handle this callback copying data between the managed and unmanaged buffers and firing a managed event which the calling app should handle to update the buffers.
Onwards
Probably of more interest than how it works is how to use it so I'll cover that here.
To build this project you’re going to need to download the ASIO SDK. You can get that here: http://www.steinberg.de/329+M52087573ab0.html (I hope. That URL looks a little dynamic so if it doesn’t work do a Google search). As I’ve said already, I’m not allowed to distribute this for licensing reasons. All you need from the SDK is the Asio.h
header file which contains a load of definitions and other stuff. Copy this into the project and you should be able to build.
The demo application shows how to iterate through available drivers and to choose one. Firstly, select a driver:
AsioDriver driver = AsioDriver.SelectDriver(
AsioDriver.InstalledDrivers[driverNumber - 1]);
This might look strange in that you could just pick a driver from the InstalledDrivers
array. Only one ASIO driver can be active at once though, and you need to select it to kick it into action.
Next, create the managed buffers
driver.CreateBuffers();
..and add an event handler to update the buffers..
driver.BufferUpdate += new EventHandler(AsioDriver_BufferUpdate);
Stick your code in the event handler to manipulate the buffers and follow with a call to:
driver.Start();
We're in business.
The Demo Application
Now for the stupid bit. A simple way to test our assembly out would be to feed the input back to the output, with a slight delay so we can distinguish what’s played back from what went in. A while back I was talking on Skype to my girlfriend who was working out in Australia at the time, and there was a lag on the line. I heard an echo of myself. Strangely, I could hardly speak - hearing my own voice with a delay on it perplexed my brain. The demo app has exactly the same effect. I got some volunteers to try reading a bit of text while wearing a USB headset running the app and much to my amusement they all turned into incoherent stuttering fools. I’m glad to report that it usually stops when you take the headset off.
Next Stage
It would have been cool to present a full article on a software synth, but unfortunately the subject is just too big to fit into one article. Hopefully I'll get round to it soon and put the component here to better use.
Three more things...
The application has only been tested against the driver ASIO4ALL. If you're using a different driver and it doesn't work it may be that the driver is using a different underlying sample format. Drop me a line if you have problems.
The code was built using Visual Studio 2008. There's a couple of generic lists used here which baseline the code at .NET 2.0, but should you still be running .NET 1.1 you could easily alter these. If you're not using VS2008 you may have to create a new solution and add the files manually - I doubt the solution file will be backwards compatible.
ASIO is a trademark and software of Steinberg Media Technologies GmbH
History
21 March 2008 - Initial Version