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.
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 do 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 of 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 bowfins 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 Web site 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 an indexer which wraps up the unmanaged buffers. A system of double buffering is used, so while one buffer plays or captures, we update the other.
ASIO supports various different sample formats, but this implementation only uses one, 32 bit signed integers. I wasn't too keen on exposing this to our calling app, so instead, we expose floats which have a permissible range of -1.0 to 1.0. When we read or write to the ASIO buffer, we do a conversion back and forth.
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 by switching the update buffer (double buffering) and firing a managed event which the calling app should handle to update the buffer.
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. (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 activated at once though, and you need to select it to kick it into action.
Next, create the managed buffers:
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:
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.
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 around 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 are 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 Visual Studio 2008, 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.
- 21st March, 2008 - Initial version
- 17th April, 2008 - Various bug fixes and improvements
I've updated the source to include various improvements and fixes. Some people found that the component wouldn't work with particular cards as well as spotted some general sloppiness and have helped track down and fix the issues. This update is almost entirely the work of Sieds Tilstra, so many thanks go to him. Enhancements include:
- The use of
CoInitialize to initialise COM, I forgot this and will live forever with the shame
- The removal of the managed buffers, Indexers now write and read direct from the unmanaged buffers
- Adjustment of sample range to be what it should be: -1.0 to 1.0. Previously it was always positive
- Range checking on samples to avoid nasty clicks
- The ability to deselect a driver. This couldn't be done previously