Click here to Skip to main content
15,892,005 members
Articles / Desktop Programming / WTL

Multidevice ASIO output plugin for WinAMP

Rate me:
Please Sign up or sign in to vote.
4.80/5 (9 votes)
13 Feb 2009CDDL27 min read 48.2K   728   23  
A tiny WinAMP output DLL that uses a C++ replacement of the official ASIO SDK that supports multiple ASIO devices.
#pragma once

#include "COMSmartPtrWrapper.hpp"
#include "Registry.hpp"
#include "tlsObject.hpp"

#include "ASIO SDK/common/iasiodrv.h"
#include "Dbghelp.h"

#include <cassert>
#include <string>


////////////////////////////////////////////////////////////////////////////////
//
//  ImportsHooker
//  -------------
//
//  Wraps the functionality for hooking DLL (ex/im)ports. Based on code from
// Jeffery Richter's "Programming Applications for Microsoft Windows (4th
// edition)".
//
//  Use of Create/factory methods is a quick-fix for lack of delegating
// constructors.
//
// (todo) currently only a 'helper class', move to a separate/appropriate
//        module.
// (todo) refactor to lazy load the dbghelp.dll or remove the dbghelp.dll
//        dependency completely and use __ImageBase directly.
//
////////////////////////////////////////////////////////////////////////////////

class ImportsHooker
{
public:
    static ImportsHooker Create( TCHAR const * moduleName );
    static ImportsHooker Create( HMODULE moduleHandle );

    bool switchFunctionThunk( PCSTR originalFunctionImplementingModuleName, LPCSTR pOriginalFunctionName, void * pNewFunction ) const;
    bool switchFunctionThunk( PCSTR originalFunctionImplementingModuleName, void * pOriginalFunction    , void * pNewFunction ) const;

private:
    ImportsHooker( PIMAGE_IMPORT_DESCRIPTOR const pImportDesc, BYTE * const pModuleOffset ) : pImportDesc_( pImportDesc ), pModuleOffset_( pModuleOffset ) {}
    PIMAGE_IMPORT_DESCRIPTOR findImportDescriptorForModule( PCSTR module ) const;
    void operator=( ImportsHooker const & );

private:
    PIMAGE_IMPORT_DESCRIPTOR const pImportDesc_;
    BYTE * const pModuleOffset_;
};


////////////////////////////////////////////////////////////////////////////////
//
//  asioCOMInitializationFlags
//  --------------------------
//
//  Public definition to enable consistent (re)use.
//
////////////////////////////////////////////////////////////////////////////////

DWORD const asioCOMInitializationFlags
(
    COINIT_APARTMENTTHREADED |
    COINIT_DISABLE_OLE1DDE   |
    COINIT_SPEED_OVER_MEMORY
);


////////////////////////////////////////////////////////////////////////////////
//
//  IASIOPtr
//  --------
//
//  IASIOPtr typedef used by the rest of the code. With the use of the
// COMAutoPtrWrapper, provides a single point for switching/testing different
// COM smart pointer implementations.
//
////////////////////////////////////////////////////////////////////////////////

typedef _com_ptr_t<_com_IIID<IASIO, &IID_IUnknown>> NativeIASIOPtr;
typedef ATL::CComPtr<IASIO>                         ATLIASIOPtr   ;
typedef IASIO *                                     RawIASIOPtr   ;

typedef COMAutoPtrWrapper<RawIASIOPtr, IASIO> IASIOPtr;


////////////////////////////////////////////////////////////////////////////////
//
//  ASIODriverList
//  --------------
//
//  A thin extension to the SubKeys class which automatically opens/selects the
// ASIO registry key and provides the getDriverData() member function which
// searches for a driver and returns the first one if the requested one isn't
// found.
//
////////////////////////////////////////////////////////////////////////////////

class ASIODriverData;

class ASIODriverList : public SubKeys
{
public:
    ASIODriverList();

    ASIODriverData getDriverData( TCHAR const * driverName ) const;
};


////////////////////////////////////////////////////////////////////////////////
//
//  ASIODriverData
//  --------------
//
//  Stores and wraps access to data available in a driver's registry
// (sub)key(s).
//
////////////////////////////////////////////////////////////////////////////////

class ASIODriverData
{
public:
    ASIODriverData( ASIODriverList::const_iterator const & );
    ASIODriverData( ASIODriverList const &, ASIODriverList::size_type driverIndex );
    ASIODriverData( HKEY const driverKey ) { readDriverData( driverKey ); }
    ASIODriverData( HKEY hASIORootKey, TCHAR const * driverKeyName );

    CLSID        const & classID    () const { return classID_; }
    OLECHAR      const * classIDStr () const { return classIDStr_; }
    std::wstring         classIDStr2() const { assert( classIDStr()[ clsidStrLen ] == '\0' ); return std::wstring( classIDStr(), clsidStrLen ); }
    TCHAR        const * description() const { return description_; }

    WinString path () const;
    tstring   path2() const;

    IASIOPtr createInstance() const;

public:
    static size_t const clsidStrLen   = 38;
    static size_t const maxDrvNameLen = 128;

private:
    void readDriverData( HKEY driverKey );

private:
    CLSID	classID_;
    OLECHAR classIDStr_[ clsidStrLen + 1 ];
    TCHAR	description_[ maxDrvNameLen ];
};


////////////////////////////////////////////////////////////////////////////////
//
//  ASIODriver
//  ----------
//
//  First wrapper layer around the IASIO interface. Besides the various
// overloads that try to fix some of the ugliness, it keeps track of the state
// of the driver/device (as documented/drawn in the ASIO SDK) and offers a few
// named constants/enums for some 'magic numbers' found in the SDK.
//
////////////////////////////////////////////////////////////////////////////////

class ASIODriver
{
public:
    enum State
    {
        Unloaded = false,
        Loaded = true,
        Initialized,
        Prepared,
        Running
    };

    enum Gain
    {
        minusInf_dB = 0,
        zero_dB     = 0x20000000,
        twelve_dB   = 0x7fffffff
    };

    enum Pan
    {
        left   = 0,
        right  = 0x7fffffff,
        centre = ( right - left ) / 2
    };

    ////////////////////////////////////////////////////////////////////////////
    //
    //  ASIOCallbacks
    //  -------------
    //
    //  Redefinition of the ASIOCallbacks struct from asio.h to add the __cdecl
    // specifier needed to compile with a default calling convention different
    // from cdecl.
    //
    ////////////////////////////////////////////////////////////////////////////

    typedef void       (__cdecl * BufferSwitchCallback        )( long doubleBufferIndex, ASIOBool directProcess );
    typedef void       (__cdecl * SampleRateDidChangeCallback )( ASIOSampleRate );
    typedef long       (__cdecl * AsioMessageCallback         )( long selector, long value, void * message, double * opt );
    typedef ASIOTime * (__cdecl * BufferSwitchTimeInfoCallback)( ASIOTime *, long doubleBufferIndex, ASIOBool directProcess );

    #pragma pack(push,4)
    struct ASIOCallbacks
    {
        BufferSwitchCallback         bufferSwitch        ;
        SampleRateDidChangeCallback  sampleRateDidChange ;
        AsioMessageCallback          asioMessage         ;
        BufferSwitchTimeInfoCallback bufferSwitchTimeInfo;
    };
    #pragma pack(pop)

public:
    ASIODriver() : state_( Unloaded ) {}
    ASIODriver( IASIOPtr const & pDriver ) : pDriver_( pDriver ), state_( static_cast<State>( static_cast<bool>( pDriver ) ) ) {}

    // Initialization
    State loadDriverCOMObject( ASIODriverData const & driverData );
    State initializeDriverCOMObject( HWND parentWindow );
    void  unloadDriverCOMObject();
    // Initialization helper wrapper
    State        initialize( ASIODriverData const & driverData, HWND parentWindow );
    char const * getInitializationErrorMessage();

    char const * getDriverName   () const;
    long         getDriverVersion() const { return driver().getDriverVersion(); }
    char const * getErrorMessage () const;

    bool can( long kAsioCanXEnum ) const;

	bool      canSampleRate( ASIOSampleRate const & sampleRate ) const { return driver().canSampleRate( sampleRate ) == ASE_OK; }
	ASIOError getSampleRate( ASIOSampleRate       & sampleRate ) const { return driver().getSampleRate( &sampleRate ); }
    double    getSampleRate()                                    const { ASIOSampleRate sampleRate; VERIFY( getSampleRate( sampleRate ) == ASE_OK ); return sampleRate; }
	ASIOError setSampleRate( ASIOSampleRate const & sampleRate )       { return driver().setSampleRate( sampleRate ); }

    ASIOError getBufferSize( long & minSize, long & maxSize, long & preferredSize, long & granularity ) const { return driver().getBufferSize( &minSize, &maxSize, &preferredSize, &granularity ); }
    long      fixBufferSize( long bufferSize = 0 ) const;
    ASIOError createBuffers( ASIOBufferInfo bufferInfos[], long numChannels, long bufferSizeInSamples, ASIOCallbacks & callbacks );
    ASIOError disposeBuffers();

    ASIOError getClockSources( ASIOClockSource clockSources[], long & numSources ) const { return driver().getClockSources( clockSources, &numSources ); }
    ASIOError setClockSource ( long const clockSourceIndex )                       const { return driver().setClockSource ( clockSourceIndex ); }

    ASIOError getLatenciesInSamples( long & inputLatency,     long & outputLatency     ) const { return driver().getLatencies( &inputLatency, &outputLatency ); }
    ASIOError getNumberOfChannels  ( long & maxInputChannels, long & maxOutputChannels ) const { return driver().getChannels( &maxInputChannels, &maxOutputChannels ); }

    ASIOError getSamplePosition( unsigned long long & samplesPlayed, unsigned long long & nsTimeStamp ) const;
    ASIOError getSamplePosition( ASIOTime & asioTime ) const;

    ASIOError      getChannelInfo( ASIOChannelInfo & info ) const { return driver().getChannelInfo( &info ); }
    ASIOSampleType getSampleType ( ASIOBool const forInputChannel = ASIOFalse ) const;
    bool           allChannelsAreOfSameType() const;
    
	bool start();
	bool stop ();

    bool timeCodeRead   ( bool const enable ) { return futureCall( enable ? kAsioEnableTimeCodeRead : kAsioDisableTimeCodeRead, NULL ) == ASE_SUCCESS; }

    bool setInputMonitor( long inChannel, long outChannel, long gain = zero_dB, bool turnOn = true, long pan = centre );

    bool setChannelGain ( long channel, long gain = zero_dB, bool isInput = false );

    ASIOError futureCall( long const selector, void * const opt ) { return driver().future( selector, opt ); }
    ASIOError openControlPanel() const { return driver().controlPanel(); }
    ASIOError outputReady() const { return driver().outputReady(); }

    State state() const { assert( static_cast<bool>( pDriver_ ) == ( state_ >= Loaded ) ); return state_; }

    typedef bool (ASIODriver::*unspecified_bool_type)();
    operator unspecified_bool_type() const { return pDriver_ ? &ASIODriver::start : 0; }

private:
    //  The copy constructor is not disabled because it is required by
    // STL containers although it should be because this class is not
    // designed to be copyable (because, then, one instance could change the
    // state of the underlying device/driver behind the back of a 'copy'
    // working with the same driver).
    //  This will be fixed with boost::intrusive_containers (or "someday"
    // with C++(0/1)x move semantics). Until then make sure that the copy
    // constructor is used only by STL containers in insert-like operations.
    //ASIODriver( ASIODriver const & );

    IASIO & driver() const { assert( pDriver_ ); return *pDriver_; }

private:
	IASIOPtr pDriver_;
    State    state_  ;
};


////////////////////////////////////////////////////////////////////////////////
//
//  ASIODevice
//  ----------
//
//  The second wrapper layer around the IASIO interface. Extends the ASIODriver
// class with functionality related to ASIO buffers and callbacks. A template
// class to eliminate runtime allocation of the buffer pointers array to avoid
// the extra pointer dereferencing in buffer switch callbacks.
//
// (todo) Try to further split the class to minimize template related in-header/
//        in-place implementation details ugliness.
//
////////////////////////////////////////////////////////////////////////////////

template <unsigned maxInChannels, unsigned maxOutChannels>
class ASIODevice : public ASIODriver
{
public:
    ASIODevice() { setCallbacks( NULL, NULL, NULL, NULL ); }
	ASIODevice
    (
        BufferSwitchTimeInfoCallback ,
        BufferSwitchCallback         ,
        SampleRateDidChangeCallback  ,
        AsioMessageCallback          ,
        IASIOPtr pDriver = IASIOPtr(),
        HWND parentWindow = NULL
    );
    ASIODevice
    (
        BufferSwitchTimeInfoCallback ,
        BufferSwitchCallback         ,
        SampleRateDidChangeCallback  ,
        AsioMessageCallback          ,
        ASIODriver const &
    );
	~ASIODevice() {}

    ASIOError createBuffers( size_t numInChannels, size_t numOutChannels, size_t bufferSize );

    size_t setOutputFormat( ASIOSampleRate, size_t numOutChannels = 2, size_t numInChannels = 0, size_t requestedBufferSize = 0 );

    void setCallbacks( BufferSwitchTimeInfoCallback, BufferSwitchCallback, SampleRateDidChangeCallback, AsioMessageCallback );
    void setCallbacks( ASIOCallbacks const & callbacks ) { callbacks_ = callbacks; }

    bool supportsCallbackSwitching();
    bool startUsingCallbackSwitching();
    bool start();

    static void hookDriverCreateThread( TCHAR const * driverModulePath );

    typedef BYTE const * InputBuffers [ maxInChannels  ? maxInChannels  : 1 ][ 2 ];
    typedef BYTE       * OutputBuffers[ maxOutChannels ? maxOutChannels : 1 ][ 2 ];
    
    InputBuffers  const & inBuffers ()  const { return inBuffers_ ; }
    OutputBuffers const & outBuffers()        { return outBuffers_; }

    static ASIODevice & getThreadInstance() { return TLSObject<ASIODevice>::getThreadLocal(); }

private: // (todo) clean this up, document and remove from the header.

    ////////////////////////////////////////////////////////////////////////////
    //
    //  TLSObject-setting-through-CreateThread-hooking functionality.
    //
    ////////////////////////////////////////////////////////////////////////////

    struct OriginalThreadParameters
    {
        OriginalThreadParameters( LPTHREAD_START_ROUTINE const lpStartAddress, LPVOID const lpParameter ) : lpStartAddress( lpStartAddress ), lpParameter( lpParameter ) {}
        void operator=( OriginalThreadParameters const & );
        LPTHREAD_START_ROUTINE const lpStartAddress;
        LPVOID                 const lpParameter   ;
    };

    static DWORD WINAPI CreateThreadHook( LPVOID const pOriginalParameters )
    {
        OriginalThreadParameters const originalParameters( *static_cast<OriginalThreadParameters const *>( pOriginalParameters ) );
        delete pOriginalParameters;
        TLSObject<ASIODevice>::setThreadLocal();
        return originalParameters.lpStartAddress( originalParameters.lpParameter );
    }
    static HANDLE WINAPI CreateThreadPassThrough
    (
        LPSECURITY_ATTRIBUTES  const lpThreadAttributes,
        SIZE_T                 const dwStackSize,
        LPTHREAD_START_ROUTINE const lpStartAddress,
        LPVOID                 const lpParameter,
        DWORD                  const dwCreationFlags,
        LPDWORD                const lpThreadId
    )
    {
        OriginalThreadParameters * const pOriginalThreadParameters( new OriginalThreadParameters( lpStartAddress, lpParameter ) );
        return ::CreateThread( lpThreadAttributes, dwStackSize, CreateThreadHook, pOriginalThreadParameters, dwCreationFlags, lpThreadId );
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    //  "Buffer switch" helper callbacks used for testing callback switching
    // capability.
    //
    ////////////////////////////////////////////////////////////////////////////

    static void __cdecl callbackSwitchCapabilityHelper1( long, ASIOBool )
    {
        assert( TLSObject<ASIODevice>::isGlobalSet() );
        if ( TLSObject<ASIODevice>::getGlobal().callbacks_.bufferSwitch == callbackSwitchCapabilityHelper2 )
        // It is not the first call (callbacks already switched but the old
        // one got called): test failed.
        {
            assert( TLSObject<ASIODevice>::getGlobal().callbacks_.bufferSwitchTimeInfo == callbackSwitchCapabilityHelperTI2 );
            //assert( !TLSObject<ASIODevice>::isThreadLocalSet() );
            lastCallbackSwitchCapabilityTestPassed_ = false;
            WinHandle const testEvent( ::OpenEvent( EVENT_MODIFY_STATE , false, tlsSynchronizationEventName ) );
            TLSObject<ASIODevice>::setThreadLocal();
            VERIFY( ::SetEvent( *testEvent ) );
        }
        else
        {
            TLSObject<ASIODevice>::getGlobal().callbacks_.bufferSwitch         = callbackSwitchCapabilityHelper2;
            TLSObject<ASIODevice>::getGlobal().callbacks_.bufferSwitchTimeInfo = callbackSwitchCapabilityHelperTI2;
        }
    }
    static ASIOTime * __cdecl callbackSwitchCapabilityHelperTI1( ASIOTime *, long, ASIOBool ) { callbackSwitchCapabilityHelper1( 0, 0 ); return NULL; }

    static void __cdecl callbackSwitchCapabilityHelper2( long, ASIOBool )
    {
        // Switched callback called: Test passed.
        lastCallbackSwitchCapabilityTestPassed_ = true;
        WinHandle const testEvent( ::OpenEvent( EVENT_MODIFY_STATE , false, tlsSynchronizationEventName ) );
        TLSObject<ASIODevice>::setThreadLocal();
        VERIFY( ::SetEvent( *testEvent ) );
    }
    static ASIOTime * __cdecl callbackSwitchCapabilityHelperTI2( ASIOTime *, long, ASIOBool ) { callbackSwitchCapabilityHelper2( 0, 0 ); return NULL; }

    ////////////////////////////////////////////////////////////////////////////
    //
    //  "Buffer switch" helper callbacks used for starting ASIO devices and
    // automatically setting the TLSObject through callback switching.
    //
    ////////////////////////////////////////////////////////////////////////////

	static void __cdecl SetTLSDeviceHelper( long const doubleBufferIndex, ASIOBool const directProcess )
	{
		assert( doubleBufferIndex == 0 );

		ASIODevice & device( TLSObject<ASIODevice>::setThreadLocal() );
        assert( device.originalBufferSwitchTimeInfo_ == NULL );
        device.callbacks_.bufferSwitch = device.originalBufferSwitch_;
        device.callbacks_.bufferSwitchTimeInfo = device.originalBufferSwitchTimeInfo_;
        
        device.originalBufferSwitch_( doubleBufferIndex, directProcess );
	}

    static ASIOTime * __cdecl SetTLSDeviceHelperTI( ASIOTime * const params, long const doubleBufferIndex, ASIOBool const directProcess )
    {
        assert( doubleBufferIndex == 0 );

        ASIODevice & device( TLSObject<ASIODevice>::setThreadLocal() );
        //assert( device.originalBufferSwitch_ == NULL );
        device.callbacks_.bufferSwitch = device.originalBufferSwitch_;
        device.callbacks_.bufferSwitchTimeInfo = device.originalBufferSwitchTimeInfo_;

        return device.originalBufferSwitchTimeInfo_( params, doubleBufferIndex, directProcess );
    }

private:
    OutputBuffers outBuffers_;
    InputBuffers  inBuffers_ ;

    ASIOCallbacks                callbacks_;
    BufferSwitchCallback         originalBufferSwitch_;
    BufferSwitchTimeInfoCallback originalBufferSwitchTimeInfo_;

    static bool lastCallbackSwitchCapabilityTestPassed_;
    static TLSObject<ASIODevice> tlsDevice_;

    // (todo) A named synchronization object should not be needed. Clean this up.
    static TCHAR const * const tlsSynchronizationEventName;
};


TCHAR const * convertASIOSampleTypeToString( ASIOSampleType );


#include "ASIODevice.inl"

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Software Developer Little Endian Ltd.
Croatia Croatia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions