Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WTL

Multidevice ASIO output plugin for WinAMP

4.80/5 (9 votes)
13 Feb 2009CDDL27 min read 49.2K   732  
A tiny WinAMP output DLL that uses a C++ replacement of the official ASIO SDK that supports multiple ASIO devices.

Wasiona-Configuration.png

Introduction

Should one wonder, from the number of acronyms/technologies mentioned in this article, there actually is a core theme from which the others sprang and this is ASIO (the audio related one from Steinberg, not the network related Boost library).
Having a passion for demanding low-level C++ programming and high fidelity digital audio, fiddling with ASIO seemed like a good idea to test and improve my skills. As this is not a purely theoretical article, there is an actual binary product of the following story, a working ASIO output plugin for WinAMP. Achieving that goal while trying not to just reinvent the wheel or repeat existing solutions but to do it better and/or differently makes the meat of this article and the source code behind it.
The sad reality of most libraries, that there is always 'something wrong with them' (be it a matter of objective flaws or subjective preferences), can actually turn out to be a 'positive' driving force in learning projects like this one. The most important ‘flaw’, which inspired this project and the wish to overcome it, was the official statement of the ASIO SDK that it does not support multiple (active) ASIO drivers. The second, or better, a ‘parallel’ one, is the ugliness and the bad/outdated/’typical C-style’ design of most APIs ‘out there’ or, on the other hand, the bloatiness of many, more modernly designed, libraries that usually reek of the ‘oh who cares, today’s computers are fast enough’ mentality. I tried to test and prove that the two poles are not the only available approaches and that code that is both efficient AND follows modern design patterns and idioms, or in other words, that is relatively easy to read and handle for both the developer AND the hardware, is actually possible. For the said reason you’ll see me ignoring Knuth, Hoare and Sutter and doing the ‘root of all evil’, with pleasure. But before we dive into it all, a ‘little’ background is in order...

Background

In short, ASIO is an acronym for Audio Stream Input Output and is a technology developed and publicly released by Steinberg Media Technologies GmbH that tries to overcome inherent flaws in previous APIs for audio streaming that prevented the creation of low latency, sample accurate software applications for the personal computer (the sample accurate part of course being more important for simple playback applications like this one). This article supposes that the reader already has some knowledge of the API and the terms connected to it.

The 'effective' side of the story.

Give me my this.

As mentioned in the introduction, one of the first things that I noticed in the SDK documentation was the one about the ‘unfortunate’ support for only a single active device/driver. Looking closely two culprits are identified. The first one being the buffer switch callbacks that typically lack the ‘void * pUserData’ ‘emulation’ of the this pointer and the other one being the implementation of the SDK itself that, in typical C fashion, uses a global variable to store the pointer to the active driver’s COM interface.

The first problem is not directly solvable as it is part of the binary specification of the API so a workaround must be used. As each driver/device creates its own thread in which the buffer switch callbacks run, the usual and obvious method is to use thread local storage to store a this pointer (to an ASIO device instance). For this ‘thread local singleton’ pattern of sorts the TLSObject<> class is used. That class only wraps the TLS implementation and synchronization of the set-global then set-thread-local usage pattern and does not solve the problem of when to set the TLS value. If we discard the option of checking whether the TLS pointer is set every time in the buffer switch callback and setting it if it is not, we are left with two solutions. One is to use the ’works for everything’ approach and hook the CreateThread function kernel32.dll import in the driver’s DLL. Theoretically this is not perfectly bullet-proof as the driver’s DLL, theoretically, need not be a dedicated COM server and host only the driver so we could intercept unwanted CreateThread calls. This method uses the ImportsHooker class (which wraps the API/imports hooking code) and sets the TLS value in a CreateThread pass-through helper function.

The other solution is provided by the way ASIO callbacks are set/passed to the driver (you fill a struct of function pointers and pass it to the driver). The window of opportunity comes from the fact that some drivers do not copy this struct but only the pointer to the struct. This does cost the driver an additional pointer dereference on each callback call and us/’hosts’ the fuss of having to save the struct for the entire duration of the playback, but it also provides the possibility to switch/change the callbacks at runtime while the playback thread is active. This is used to start the playback with helper pass through callbacks that set the TLS value the first time they are invoked and then switch the callback pointers to point to the proper buffer switch functions. This method of course does not work for drivers that copy the ASIOCallbacks struct.
Only later in the development did the idea of thunks come to my attention and this will probably be the solution that will be implemented and used in the next release as it seems both simpler to use and more efficient (maybe not so compared to a native TLS implementation but unfortunately native TLS cannot be used anyway on pre-Vista Windows in DLL projects like this one).

The second problem, related to the ASIO SDK, is solvable by bypassing/discarding the ASIO SDK’s C wrappers around the IASIO COM interface, using the COM interface directly and/or wrapping it properly in a class. Because of other flaws in the original ASIO SDK, like the rather ugly driver list which only ‘reinvents the wheel’ (i.e. std::list), I decided to completely discard it and try and make a better one from scratch, using only enums and type definitions from the original SDK.

WA <-> ASIO interaction

Before going into the details and usage of the ‘ASIO++ SDK’ we should first look at the background of the rest of the Wasiona code. Most of the meat comes from the interaction with and between the WinAMP SDK (‘yet another ugly C API’) and the ASIO(++) SDK. As one might expect the two do not just fit in nicely with each other but require a certain level of adaptation. The biggest problem comes from the fact that WinAMP uses the outdated system of polling. It (the input plugin) constantly polls the output plugin (‘us’) “How much can I write?” and then writes if the size returned by the CanWrite() callback is big enough or sleeps for an unspecified number of milliseconds if it’s not and polls again. This of course does not work properly with ASIO buffer switch callbacks (or any low latency use for that matter) because they must return within latency-number-of-milliseconds and that can be as low as one millisecond, far shorter than the usual sleep period used by input plugins in their polling and writing loop. Secondly, the WinAMP write callback uses the interleaved single-buffer format to transfer the samples while the ASIO API uses a separate buffer for each audio channel thus requiring a deinterleaving process. And thirdly, ASIO devices/drivers have a fixed output sample type so a sample type conversion process must also take place (for example 16 bit integer to 32 bit float), ‘exotic’ sample rates might also require resampling. The usual unhappy solution for all three problems is, of course, to introduce a new buffer between the two, doing all the conversions necessary in the WinAMP Write() callback and saving the completely processed data into the intermediate buffer minimizing the work needed in the buffer switch callbacks.

As in all audio applications, the intermediate buffer of course needs to be a circular buffer. The implementation chosen here (wrapped/provided by the MultiChannelBuffer class) does not use double pointers (for example like DirectSound) because this incurs additional if-then clauses/code paths and doing the copying in two passes and that diminishes the potential and benefits of using vector/SIMD instructions to process/copy the data. Instead it relies on using a buffer size several times larger then the chunks being read/written and then copying/moving the remaining one or two ‘small’ chunks at the end of the buffer to the beginning of the buffer when the end of the buffer is reached. This uses more memory at the benefit of CPU time as the extra memcpy() call (per channel) should be negligible if it occurs rare enough (the total buffer size is much larger than the chunks being read/written) and the benefit is of always having a linear/’unbroken’ buffer ‘ahead’. Adjusting the buffer size, therefore, we adjust the tradeoff between memory and CPU usage. A single physical buffer is used for all channel buffers (sizeof() = sizeof( channel ) * numberOfChannels) to improve locality of reference.

However, it turned out that this design (with a single pointer and a ‘sporadic’ memcpy() instead of two pointers) has a problem with WinAMP and its visualization system. It seems that there is some odd coupling between the visualization timing and the ‘timing’ of the stream of writes from the input plugin to the output plugin. In the described MultiChannelBuffer (MCB) design this stream does not ‘flow’ at a constant pace. Instead, what happens is that the input plugin fills the MCB relatively quickly and then waits (as the MCB denies any further writing) until the data is consumed and the MCB does the move-to-the-beginning at which point the (almost) entire buffer becomes available again for the input plugin to fill, which is exactly what it does, again in a relatively short time. So periods of close together writes alternate with idle periods and that seems to ‘confuse’ WA’s vis logic and it starts to stutter. Currently, a quick workaround has been added to the CanWrite() callback that does not allow any further writing if written data is more than 50 ms ahead of played data which keeps the written time and output time values close enough so visualization is smooth again.

Having multiple ASIO devices means that the buffer will be read from by more than one reader/consumer from different threads at different moments and in differently sized chunks. This requires special logic to keep track of the state of the buffer (the read part, the written part and the unused part) and to solve issues with access from different threads. The problem of multiple readers is modelled with a MultiChannelBuffer::Reader member class through which the state/position of each reader is tracked (in a relation similar to that of an iterator and a container in the secure version of Dinkumware’s STL) so that each reader can access its unread data and the buffer can know which data has been read by all readers (and is safe to overwrite).

The GUI

There is also a part of every WinAMP plugin, that as such is not directly connected to ASIO, and that is the (configuration) GUI. While investigating lightweight/efficient solutions I decided to give WTL a try. Having the potential to be what MFC should have been I had high hopes for it but it did kind of disappoint me at the end. It looks as if ‘compatibility’/similarity to MFC was a higher priority than modern design so it has dragged in some of the ugliness on the design level, it still uses ATL constructs instead of STL, non encapsulated public member access, (leftover) pointers instead of references (and similar C-style legacy), predeclared global ‘module objects’ and it (as do, of course, most other libraries) makes certain, non-configurable, design-vs-efficiency choices that are not necessarily ‘good for everyone’ or needed for smaller projects like this one (ranging from things like ‘manually’ precreating property sheet pages, always switching code paths depending on the state of a window even if that is compile-time knowledge to the developer to bringing in SEH, ATL container code for various COM, window, ATL etc. data, virtual functions, critical sections and custom memory allocators.
As is understandable/normal, it also has certain bugs that also affected this project. For instance the AtlCreateSimpleToolBar() / CFrameWindowImplBase<>::CreateSimpleToolBarCtrl() function does not work correctly with vertical toolbars (it does not properly set the button wrap state) but the most important issue was to get the PropertySheet 'control' to work as a dialog box child (and afterwards to also make it use the XP style white background). Fixes for these problems as well as other GUI/WTL helpers and/or 'nicer' implementations can be found in the functions and classes in the GUI module. I will not clutter this text with the details as I think the code and its usage should be clear enough from the interface, comments and usage in the ConfigDlg module.

Volume control

Finally, for the more important issues at least, there was the problem of volume control. ASIO drivers are not obliged to support the kAsioSetOutputGain futureCall()’ so an alternate method must be used to change the volume. Windows provides the waveOut/MME and mixerAPI APIs and waveOutSetVolume() seemed like the ‘default’ choice but it turns out that some (ASIO) sound cards driver’s do not support the legacy MME API so those functions have no effect with them (even the Windows volume control does not function properly). Using the mixerAPI to control the main volume in such cases can be the ‘alternate alternate’ solution. So I included all three options (MME, mixerAPI and ASIO) from which the user can choose which API to use to control volume and panning. Unfortunately, the mixerAPI is one of the ugliest APIs I had to work with and I could not get the panning to work with it no matter what (if anyone has managed to do it I will appreciate the help/input). The original idea was to allow configuring the volume setting method for each chosen device independently, but as it turned out, that is no easy task to accomplish. As the name of an ASIO device/driver, in theory, does not have to resemble the MME device/driver name and because an ASIO device does not have to have a MME driver at all or can have more than one MME driver or it can even be an ‘ASIO emulator’ for more than one device simultaneously (like ASIO4All) it follows that there is no deterministic link that would allow deducing an ASIO driver’s corresponding MME driver to be used for volume control. For the said reason the current, suboptimal and hopefully temporary solution is that the waveOut and mixerAPI methods control the volume of (only) the default Windows audio device.

The 'efficient' side of the story.

With performance being a major part of the whole ASIO idea, it comes as logical to invest the effort and ‘do things tightly’ in connected projects and especially in any wrapper/extension/library code.

In the parts of code that actually do time-critical processing, optimizing for speed was, of course, the primary goal. Besides ‘tiny tricks ’n’ hacks’ throughout the code and a global effort to solve threading issues by design rather than by synchronization (there are no critical sections in the entire project), all of this is concentrated in the WinAMP-input ASIO-output data path. The logic behind the MultiChannelBuffer class has already been covered, which leaves us with the Write() callbacks for different IO sample type combinations and those come down to the deinterleaving process. This seemed like a good opportunity to learn some MMX, so I decided to write an MMX version of the 16 bit to 32 bit deinterleaving and converting function. I searched for an already existing solution but could not find one, so I wrote mine from scratch and wouldn’t mind a bit if a more experienced MMX programmer would give me some feedback ;)

A rather larger amount of time was spent on space optimizations (which is what the 'tiny' part of the subtitle refers to, not the oversized configuration dialog :) as the first builds showed dissatisfactory results even with some of the more conventional methods of ‘rationalization’ (that will be mentioned shortly) being used from the start.
I tried to tackle the issue from different angles. The replacement “ASIO++ SDK”, as well as other code throughout the project, relies on templates and small(er) inline functions to make compile-time knowledge stay compile-time knowledge. This also has the benefit of making the code more readable by grouping code into more smaller functions. As will probably be obvious from the code (or the disassembly window), the wrappers around the IASIO COM interface are almost completely transparent to the optimizer/inliner while the rest of the SDK will, hopefully, provide both tighter binary and more powerful and easier to use source code than the original SDK.

An important decision in this regard was the choice of a COM smart pointer class. The MSVC ‘native’ _com_ptr<> actually turned out to be the worst choice because of redundant paranoia checks and use of C++ exceptions. The ATL's CComPtr<> was slightly better/’lighter’ but still unsatisfactory, so I decided to write a custom tailored one called COMSmartPtrWrapper. As the name implies, it can wrap a raw COM interface pointer as well as other COM smart pointer classes, so it can be used for an easy single point of configuration to choose and test different (smart) pointer implementations. I tried to minimze the use of bloated STL classes, namely std::string and std::vector (streams were of course not used) and managed to completely remove std::strings with one std::vector remaining in the MCB and that one will also probably be removed in the next release.

With the more conventional methods exhausted, as was mentioned earlier, the results were still unsatisfactory and the difference between the static and dynamic linking with the CRT showed the main culprit. It was time to turn to the alternative provided by the “Tiny C Runtime Library” project by Mike_V. The out of the box product was not quite sufficient mainly because of Dinkumware’s STL and ATL’s coupling with Microsoft’s CRT and other C++ features used by the project. To solve this I modified both my code and the TLibC library. TLibC was unfortunately modified without Mike_V’s knowledge or consent (when I get the time I will try and contact him to perhaps unify our efforts).

The following is the list of changes done to TLibC:

  • alloc.cpp – changed it to use Windows XP’s low fragmentation heap and call GetProcessHeap() only once.
  • file.cpp - refactored it to no longer require manual initialization of STD handles via _init_file().
  • initterm.cpp – refactored it to no longer use a dynamically allocated at-exit function pointer list as it still used a fixed size for the list; replaced the two index variables with a single pointer.
  • crt0t*.cpp and libct.h – made the changes required by the changes to the above two files.
  • math.cpp – added the ldiv() function.
  • memory.cpp – added a (naive ‘implementation’) of _resetstkoflw() required by alloca(); added the __sse2_available DWORD required by Microsoft asm memory routines; disabled the original ‘manual’ memory routines in favor of Microsoft’s asm optimized ones (this slightly increases the binary but I consider it well worth it).
  • newdel.cpp – minor code reuse refactoring.
  • sprintf.cpp – added minor sanity checks and the wide version of _snprintf.
  • string.cpp – added the _String_base::_Xlen(), _String_base::_Xran(), _String_base::_Xinvarg() functions required/used by Dinkumware’s std::string implementation to report/handle errors.
  • added memory_s.h and string_s.h with implementations for some of Microsofts ‘secure’ CRT functions.
  • .vcproj - converted to a VC9.0 project, tweaked optimization switches and added MASM support and Microsoft P4 memory routines .obj files and asm sources.
  • throughout the files:
    • added __declspec(selectany) specifiers to aid the linker in removing unused objects.
    • added warning disabling pragma directives.
    • added #pragma function() directives to enable compiling with “Enable Intrinsic Functions” set to “On”.
    • did various minor stylistic changes and refactorings.

On “my side of the story” I firstly concentrated on removing C++ exceptions and the behind-the-scenes code associated with them. I did not use them personally but the STL and ATL libraries did, so just trying to compile without C++ exception support did produce quite a bit of wining by the compiler. However, before going into what I did an apologetic explanation as to why I did it is probably in order. In this project the only (C++) exception that can actually get thrown is std::bad_alloc by the STL. The relatively slim chances that the few and small allocations performed by this small DLL (with the only exception being the “big” std::vector’s in MCBs) might actually fail IMHO warrant the exclusion of the exception handling bloat, especially because there is not much that you can do in case of a std::bad_alloc in a project like this one besides trying to display a message box and then terminating. And that is not that much different/better from crashing on a null pointer with an access violation, “exiting with -1” or being terminated by Windows for an unhandled exception.

Anyways, there is of course no standard or documented way of disabling exceptions in STL and ATL but digging through the sources one can find the _HAS_EXCEPTIONS macro for the STL and the _ATL_NO_EXCEPTIONS macro for the ATL. Defining them to zero does disable C++ exceptions from being used by the two libraries but it brings in other problems (as most undocumented and probably not so polished features do). The two more important ones that come to mind were the enormous amount of warnings spewed out by the STL headers (which was relatively easier to fix with a few pragma directives added to the precompiled headers file) and the other being the non-throwing versions of the _THROW and _RAISE macros from <xstddef> that get used in the no-exceptions case and that, somewhere in their bowels, cause std::string code to be included in the binary (allocating a std::string to display a message in case of a std::bad_alloc does seem ‘strange’ I must say) which I found unacceptable so UglyHack#1 was born: a disableDinkumwareSTLExceptions.hpp header, that redefines the said macros to do a simple exit( -1 ), is forcibly included for every compilation unit effectively solving the problem. That got rid of C++ exceptions but SEH was still in the game and ATL and alloca() were to blame. Getting rid of alloca() was relatively easy but ATL did not provide for a ‘no SEH’ build and modifying ATL sources really was less than satisfactory. But all was not lost as looking more closely it can be seen that the SEH code in the atlbase.h header serves only for handling structured exceptions that might occur when locking a critical section on a pre XP version of Windows which is completely useless for this project as it does not support those versions of Windows anyway. This gave birth to a handful of new UglyHacks grouped together under the guise of a new macro called TINY_ATL (the details of which can be found in the WTL module). When it is defined a number of things change:

  • all SEH mentioning code as well as CriticalSections (as another completely useless ‘feature’ for a project that has only one window and accesses it from only a single thread) are removed from the build using typical preprocessor hacks,
  • the ATL static library is excluded from the build and the minimal required exports from it are defined in WTL.cpp: the CAtlBaseModule's constructor and destructor, and the _stdcallthunk's custom allocator and deallocator. Also the CAtlBaseModule, CAtlComModule and CAtlWinModule globals are defined with the __declspec(selectany) specifier to enable the linker to remove them if they are not used,
  • the CAppModule _Module global variable is not (automatically) defined as it adds 10 kB to the binary alone.

I know quite a few people that what would probably have ‘are you out of your mind?’ written on their foreheads after all this, but the end result of a 28,5 kB DLL (and still room for improvement) that started at 96 kB, in my mind, says that I am not (out of my mind) but that Microsoft’s CRT is bloated and simply done wrong/too coupled/monolithic (I suspect it would have helped if the static version was built with /GL and /LTCG).

On the other hand, those that don’t mind getting their hands dirty can have a look at the list of preprocessor macros in the .vcproj listed for the release build for a few other ‘hidden gems’.

Using the code

Requirements

Before using/building the project there are a few requirements that you must meet. You will need:

  • Windows XP with Service Pack 2 or higher.
  • Microsoft’s VisualC++ 9.0 SP1 or at least ‘SP0’ + the ‘Feature pack’. As the code uses TR1 functionality anything less will not work. Additionally you need to have CRT sources and ATL installed.
  • ASIO SDK 2.2
  • WinAMP SDK
  • WTL 8.0
  • Modified TLibC (included).

There are also, unfortunately, two issues that you must manually fix:

  • Because the Microsoft Macro Assembler has a bug in its command line parsing, i.e. does not support paths with spaces in the ‘include paths’ (/I) parameter, I could not use the VC macros (i.e. $(VCInstallDir)crt\src...) to specify an additional include directory required by Microsoft CRT .asm files in a way that would work out of the box on everyone’s computer (as VC macros contain long file names with spaces). Until this is fixed you will unfortunately need to manually edit the properties for the TLibC (sub)project and under Microsoft Macro Assembler/Advanced/Include Paths insert the short path to your MSVC CRT sources directory (for example c:\progra~1\sys\vs\vc\crt\src).
  • As Visual Studio solution files store relative paths to contained/referenced project files I could not decouple the locations of the “out_wasiona” and “tlibc” projects. As is, the tlibc project is located at “..\3rdParty\TLibC” relative to the WASIO.sln file so you will have to manually fix this to suit your personal source tree and directory structure. Again, I would greatly appreciate if anyone knows a smarter solution (within the Visual Studio build system).

Finaly you will need to add the paths to the used SDKs to your Visual Studio include path.

The code

Download Wasiona_source.ZIP - 263.92 KB

As far as ASIO, as the core reusable part of this project, is concerned all of the classes are currently in the ASIODevices.hpp header file.

The first on the list is the IASIOPtr typdef that was mentioned and explained earlier. One more thing worth mentioning is the ‘NativeIASIOPtr' typedef. Its declaration might seem strange to an experienced COM programmer but that was the only way I could create the typedef for the _com_ptr<> class as Steinberg have completely messed up things in their COM specification. The first mistake (perhaps not as important) is that the methods do not return HRESULTs and the second being that a driver's CLSID is also used as the IID thus rendering a (proper) driver independent smart pointer typedef impossible. While at it, I must also rant about the whole COM business in the ASIO SDK. As it aims at cross platform compatibility I must say I don't understand why they chose COM (only) for the Wintel platform (a simple DLL interface specification would have been enough) but more bugging than that is the fact that they completely missed the whole point of COM, interfaces and object oriented design. The worst examples are probably the IASIO::future() method (which is supposed to serve as an extension point for the API using the „so 80's C-style“ pattern of sending messages and void pointers and parsing them in switch cases when COM out-of-the-box provides for extending interfaces with new methods) and the fact that its all finally wrapped in a C API.

Next is the ASIODriverList class, a thin wrapper around the SubKeys class that, again, is a wrapper around a Windows Registry (sub)key that offers an STL random-access container like interface to its subkeys (unlike the alternative from the ASIO SDK it makes no allocations and does not preload the subkeys). The ASIODriverList class only automatically selects/opens the ASIO Registry key and offers one new utility member function.

Coupled with it is the ASIODriverData class which can be constructed from a driver (name) that you chose from the available ones found by the ASIODriverList. Besides offering information available for a particular ASIO driver it offers a utility member for creating an instance of the described driver.
Instead of the simple IASIOPtr returned by ASIODriverData::createInstance() you will probably want to use the, next in line, ASIODriver class that tries to offer a cleaner C++ interface as well as handy helpers and utility methods while keeping track of the driver's state (and asserting that you are using it properly).
Finally, for the actual playing, recording and streaming with your ASIO driver you’ll want to use the ‘fattest’ ASIODevice class that extends ASIODriver with the ASIO buffers, ASIO callbacks and TLS functionality. It also provides the supportsCallbackSwitching() member function which you can use to query whether the driver supports the callback switching trick that was mentioned earlier in the article. If it does you can use the startUsingCallbackSwitching() to start your device, if it does not you must first (once) call hookDriverCreateThread() and then start().

ps. As a side note it should be mentioned that there are some unused overloads of various (member) functions in different classes leftover from different stages of the code’s evolution that were not removed yet as they might become useful again or can simply be viewed as ‘library exports’.

Usage

A simple scenario of loading and starting a device would go something like this (most error handling is omitted for brevity):

// Load driver list.
ASIODriverList const drivers;
if ( drivers.empty() )
{
  // no ASIO drivers found, handle/notify...
}
// Choose the first driver (or a different one by index).
ASIODriverData const driverData( drivers[ 0 ] );
// Or find one by name.
ASIODriverData const driverData( drivers.getDriverData( "my driver" ) );
// Or search for it in a loop.
for( ... drivers.begin() ... drivers.end() ... ) { ... }
// After you have selected your driver you create an instance wrapped
// in one of the three flavours, depending on your needs:
IASIOPtr pIASIO( driverData.createInstance() );
// ...or...
ASIODriver asioDriver( driverData.createInstance() );
// ...or...
ASIODevice asioDevice
(
  ...pointers to your callback functions...,
  driverData.createInstance(),
  parentWindowHWND
);
// For most uses you will probably be able to use the utility setOutputFormat()
// member function that will also automatically create the buffers of the
// default/driver prefered size.
asioDevice.setOutputFormat( desiredOutputSampleRate, desiredNumberOfOutputChannels );
if ( asioDevice.supportsCallbackSwitching() )
  asioDevice.startUsingCallbackSwitching();
else
{
  asioDevice.hookDriverCreateThread(driverData.path().c_str() );
  asioDevice.start();
}

ps. The WinAMP code is also organized and split into classes and modules to allow reuse in other projects but its reusable parts are IMHO relatively simple and comprehendible from the code alone so I will restrain from making the article even longer.

Points of Interest, Wishes and Todos

The project of course still has a lot of ground to cover to reach maturity. Besides the various todo notes that can be found around the code, the major areas that need work can be approximately sorted as follows:

  • code cleanup
    • search for any remaining non encapsulated public member access
    • cleanup remaining inconsistencies in the naming convention (capital letters, member variable names...) in both the code and the file names
    • the project should be split into distinct libraries to allow proper (re)use as well as easier development and updating
    • currently, the project is rather heavily tied with the MSVC compiler and its specifics, it would be nice to minimize or even eliminate this (without sideffects of course).
    • make use of the Boost libraries (for example BOOST_STATIC_ASSERTs, intrusive containers...) to improve the code
  • add all the remaining and missing Write() callbacks and deinterleaving functions needed to support ‘all’ IO sample type combinations (currently only 16 bit and 24 bit input sample resolutions and the ASIOSTInt32LSB output sample type are supported), all the ground work has already been layed out and adding support for an additional IO sample type combination requires only the writing of a new specialization for the WriterImpl<outputSampleType, inputBitsPerSample>::Write() member function while template magic will automatically take care of the rest and use the new function
  • try to find a way to allow for per device volume control methods
  • add error reporting via exceptions to the ASIO++ SDK as it might be more suitable for larger projects, add a compile time switch for choosing the level and the mechanism for error handling and reporting
  • add a compile time switch (and the required support and changes to the code) to chose between a single-device and a multi-device build (currently, besides the obvious space overhead, the cost of supporting multiple devices when using only a single device almost comes down to the TLS access as the code automatically switches the WA Write() callback to access the one device directly instead of iterating over all devices), this is especially needed for the ASIO++ SDK, at least until the thunking functionality is added
  • add support for resampling
  • add support for playing files with an arbitrary number of channels, mapping those to different physical output channels, expansion and downmixing
  • add support for device synchronization and the usage of clock sources (currently only with minimum latency values can bearable experiences be achieved in multi-device scenarios), having only one ‘proper’ ASIO sound device (the Terratec firewire Aureon) and emulating the other (with ASIO4ALL and the onboard Realtek chip) I lacked the hardware with the required support to develop and test this feature
  • add crossfading functionality (like the SQRSoft’s plugin but preferably without the requirement to allocate huge buffers)

All feedback is, of course, welcome.

History

2009-02-10 First public release and submission to CodeProject.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)