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 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.

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. (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:

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:

driver.CreateBuffers(true);

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 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.

History

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

Share

About the Author

I am a .NET architect/developer based in London working mostly on financial trading systems. My love of computers started at an early age with BASIC on a 3KB VIC20 and progressed onto a 32KB BBC Micro using BASIC and 6502 assembly language. From there I moved on to the blisteringly fast Acorn Archimedes using BASIC and ARM assembly.

I started developing with C++ since 1990, where it was introduced to me in my first year studying for a Computer Science degree at the University of Nottingham. I started professionally with Visual C++ version 1.51 in 1993.

I moved over to C# and .NET in early 2004 after a long period of denial that anything could improve upon C++.

Recently I did a bit of work in my old language of C++ and I now realise that frankly, it's a total pain in the arse.

Comments and Discussions

Hello, Rob! Please help! When I try to compile the project I get the error: "The name 'AsioDriver' does not exist in the current context".
OS Win7 X64, VS 2013 Comunity, ASIO4ALL v2.11 english. I copied Asio.h (version ASIOSDK: 2.3) in the project folder. I also tried to run the project in VS2008, 2010 and 2012, but the result was the same. I also tried to run the project OS Win7 x32 in different studios, the result was the same...

Hello,
I am integrating the control panel in my application.
I use Asio4all.
I tested stop-start with nothing in the middle, it works.
But if i stop-change asio ports selection-start, i naturally verify Steinberg asumption that the real sequence should be : stop-disposebuffer-change io port selection-createbuffers-start. This is logic : changing the ports changes the buffers too !
But when restarting i get memory violation exception !

Weird !
I use Asio4all API to replicate the control panel within my application. For the test, i just read the device/interface/pins, with no change.
Until you answer me, i will try to completely realloc the asio driver after changing the ports :
init/createbuffers/start/My panel/release the driver then reopen.
Seb

Just wondering if you are still out there but do you know if it is possible to use the soundcard hardware to mix data i supply (not from microphone), from multiple channels, and then surface the stereo mix back up in memory so i can read it? I figure the sound card can mix data quicker than my cpu and i'd like to offload to it if possible.

The short answer is no I don't, I guess that would depend on the particular soundcard. To my mind ASIO is about getting audio into and out of your computer and not much more.

In terms of mixing though, I would think it would be more effort to take the individual signals and marshal them into the soundcard, let it do its stuff and take them back out again - this will also add to the latency.

In the simplest terms, all you need to do is add the signals together to mix them, possibly multiplying by some amplitude value as you do so. This is blindingly quick on modern computers and I wouldn't be concerned about that.

Hi! First, I love this project and I am using it for a substantial audio project. I am having one problem though. The buffer switch callback can easily be interrupted by other threads (most obviously the main thread of the application). When this happens the total time spent in the callback is greater than the interval between successive callbacks, to the ASIO driver is starved of data and stutters. This is especially a problem for cards that support small buffer sizes (e.g. 64 samples).

Has anyone else run into a similar problem or can offer a solution? Reliability at low latency is important for my app.

I want to implement ASIO.NET in my multithreaded application. Calling the AsioDriver::InternalSelectDriver method inside the main application thread works fine. But if I try to call that method from a different application thread, CoCreateInstance results in E_NOINTERFACE. What to do?

I think this is down to the perplexing COM threading model. Because the driver is a COM object and uses the single threaded apartment model only the thread which initialises the object is allowed to access it.

Yes, C++ can be a painful experience. The linker is telling you that it can't find some of the COM stuff. This is because you have different configurations for release and debug.

What you need to do is look in the configurations dialog. Select debug at the top and make a note of the all the .libs that are supplied as input. Switch to release and see what's missing. You can probably just copy the string of libs from debug to release.

I'm working from memory so sorry if a bit vague. In particular I can't remember the .lib which you need. Ole32.lib maybe.

When does the driver fire the event in order to fill the buffer? Is it when the A/D converter has a full buffer and before overriding the data it wants the subscriber to save the data or do whatever with it? I mean, is this event fired from the recording part of the driver/device? And what happens when the driver is started? Is playback and recording started simultaneously? What if you only want to record without playback? What if you want only playback and not record? Is there also a event necessary for playback e. g. does the driver query for new data in its playback buffer by firing a event? Thanks for any help and hints.