Click here to Skip to main content
15,867,308 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 47.9K   727   23  
A tiny WinAMP output DLL that uses a C++ replacement of the official ASIO SDK that supports multiple ASIO devices.
#include "buffers.hpp"

#include <algorithm>
#include <type_traits>


////////////////////////////////////////////////////////////////////////////////
//
//  Helpers and private implementation details.
//  -------------------------------------------
//
////////////////////////////////////////////////////////////////////////////////

namespace // anonymous
{
    //************************************
    // Method:    InterlockedAdd
    // FullName:  InterlockedAdd<T>
    // Access:    global
    //************************************

    template <typename T>
    void InterlockedAdd( T & t, long const addend )
    {
        assert( std::tr1::is_pod<T>::value );
        assert( sizeof( T ) == sizeof( long ) );
        //  Even with full optimization, intrinsics on and optimize for speed
        // the (MSVC 9.0) compiler chooses the Windows kernel implementation of
        // InterlockedExchangeAdd so here the intrinsic version is forcibly
        // called.
        _InterlockedExchangeAdd( &reinterpret_cast<long &>( t ), addend );
    }



    //************************************
    // Method:    InterlockedIteratorAdvance
    // FullName:  InterlockedIteratorAdvance<RandomAccessIterator>
    // Access:    global
    //************************************

    template <class RandomAccessIterator>
    void InterlockedIteratorAdvance( RandomAccessIterator & iterator, long const offset )
    {
    #if _SECURE_SCL || _HAS_ITERATOR_DEBUGGING
        //  Perform/force a manual check as the real modification will be done
        // "behind the checking code's back".
        RandomAccessIterator( iterator ) + offset;
        InterlockedAdd( iterator._Myptr, offset );
    #else
        assert( sizeof( firstChannelBeginning_ ) == sizeof( long ) );
        assert( std::tr1::has_trivial_destructor<RandomAccessIterator>::value );
        InterlockedAdd( iterator       , offset );
    #endif
    }
}  // namespace anonymous


//************************************
// Method:    MultiChannelBuffer
// FullName:  MultiChannelBuffer::MultiChannelBuffer
// Access:    public 
//************************************

MultiChannelBuffer::MultiChannelBuffer()
    :
    buffer_                    ( 1               ), // this actually reduces the binary by a chunk (512 bytes)...!?
    channelSize_               ( 0               ),
    bytesWritten_              ( 0               ),
    minimumSpaceThatMustRemain_( 0               ),
    writePosition_             ( buffer_.begin() )
{
}


//************************************
// Method:    allReadersHaveTheSameEndMarker
// FullName:  MultiChannelBuffer::allReadersHaveTheSameEndMarker
// Access:    private 
//************************************

bool MultiChannelBuffer::allReadersHaveTheSameEndMarker() const
{
    // (todo) This check requires atomic iterator (which is non-pod structure
    //        under secure SCL) access.
#if 0 // Disabled until fully implemented.
    Buffer::const_iterator const endMarker( writeCursor() );
    for( Readers::const_iterator pReader( ++readers_.begin() ); pReader != readers_.end(); ++pReader )
    {
        Reader & reader( **pReader );
        if ( ( reader.firstChannelBeginning_ + reader.availableDataPerChannel_  ) != endMarker )
            return false;
    }
#endif // Disabled until fully implemented.
    return true;
}


//************************************
// Method:    canWrite
// FullName:  MultiChannelBuffer::canWrite
// Access:    public 
//************************************

size_t MultiChannelBuffer::canWrite() const
{
    Buffer::const_iterator const readPosition( slowestReaderPosition()      );
    size_t                 const unreadSize  ( writeCursor() - readPosition );
    //  The unread size could be read directly from the "slowest reader"
    // ( unreadSize == slowestReader().availableDataPerChannel() ) but that
    // would require reading it atomically/locked together with its position
    // (firstChannelBeginning) because they are modified independently from
    // different threads so this is simpler to do.
    if ( unreadSize == 0 )
    {
        assert( allReadersHaveTheSameEndMarker() );

        for( Readers::const_iterator pReader( readers_.begin() ); pReader != readers_.end(); ++pReader )
        {
            //  If even the slowest reader has nothing to read then all readers
            // (must) have nothing to read.
            assert( ( **pReader ).availableDataPerChannel_ == 0            );
            assert( ( **pReader ).firstChannelBeginning_   == readPosition );
            ( **pReader ).firstChannelBeginning_ = buffer_.begin();
        }

        writePosition_ = buffer_.begin();

        assert( allReadersHaveTheSameEndMarker() );
        
        return channelSize();
    }

    //  If we are at/near the end of the buffer and the
    // minimumSpaceThatMustRemain_ threshold has been reached move the remaining
    // data to the beginning of the buffer.
    // (todo) This design and code is modeled against the WinAMP API and
    //        input-output module interaction with which a maximum size limit
    //        for write chunks is known (it is currently coupled so much that
    //        even the maxWAInputChunk/8192 deduced from the WA SDK out.h header
    //        is directly copied here instead of specified from outside). Once
    //        the 'final' design settles and the reusability potential of the
    //        MultiChannelBuffer class becomes more clear this coupling should
    //        be removed.
    size_t const spaceAtEnd( channelSize() - writeCursorOffset() );
    size_t const maxInputChunk( 8192 ); // maxWAInputChunk
    if ( ( spaceAtEnd < maxInputChunk ) && ( unreadSize < minimumSpaceThatMustRemain_ ) )
    {
        assert( allReadersHaveTheSameEndMarker() );

        size_t const spaceAtBeginning( readPosition - buffer_.begin() );

        assert( ( spaceAtBeginning > spaceAtEnd ) && "Too small buffer size chosen. minimumSpaceThatMustRemain_ should be 'much smaller' than the total buffer size" );

        assert( ( spaceAtBeginning >= unreadSize ) && "Buffer overlap. Probably a too small buffer. Requires memmove(). Thread unsafe." );
        // (todo) try to optimize this so that the source and the destinatiion
        //        are always properly aligned to for the SSE optimized routine
        //        so that VEC_memcpy() can be called directly.
        for( Buffer::iterator pChannelBuffer( buffer_.begin() ); pChannelBuffer < buffer_.end(); pChannelBuffer += channelSize() )
            std::memcpy( &*pChannelBuffer, &pChannelBuffer[ spaceAtBeginning ], unreadSize );

        for( Readers::const_iterator pReader( readers_.begin() ); pReader != readers_.end(); ++pReader )
            // "reader.firstChannelBeginning_ -= readCursorOffset;":
            InterlockedIteratorAdvance( (**pReader).firstChannelBeginning_, -static_cast<long>( spaceAtBeginning ) );

        writePosition_ = buffer_.begin() + unreadSize;

        assert( spaceAtBeginning + spaceAtEnd == channelSize() - writeCursorOffset() );

        assert( allReadersHaveTheSameEndMarker() );

        return spaceAtBeginning + spaceAtEnd;
    }

    return spaceAtEnd;
}


//************************************
// Method:    createNewReader
// FullName:  MultiChannelBuffer::createNewReader
// Access:    public 
//************************************

MultiChannelBuffer::Reader MultiChannelBuffer::createNewReader() const
{
    assert( isInResetState() );
    return Reader( buffer_.begin(), *this );
}


//************************************
// Method:    getWritePointers
// FullName:  MultiChannelBuffer::getWritePointers
// Access:    public 
//************************************

MultiChannelBuffer::WritePointers MultiChannelBuffer::getWritePointers( size_t const sz )
{
    assert( sz <= ( channelSize() - writeCursorOffset() ) );  // "Non adjusting" canWrite.
    return WritePointers( writePosition_, channelSize(), sz, *this );
}


//************************************
// Method:    hasUnreadInput
// FullName:  MultiChannelBuffer::hasUnreadInput
// Access:    public 
//************************************

bool MultiChannelBuffer::hasUnreadInput() const
{
    //return slowestReader().availableDataPerChannel() != 0;
    for( Readers::const_iterator pReader( readers_.begin() ); pReader != readers_.end(); ++pReader )
        if ( (*pReader)->availableDataPerChannel() != 0 )
            return true;
    return false;
}


//************************************
// Method:    isInResetState
// FullName:  MultiChannelBuffer::isInResetState
// Access:    private 
//************************************

bool MultiChannelBuffer::isInResetState() const
{
    for( Readers::const_iterator pReader( readers_.begin() ); pReader != readers_.end(); ++pReader )
        if ( (*pReader)->firstChannelBeginning_ != buffer_.begin() )
            return false;
    return !bytesWritten_ && ( writePosition_ == buffer_.begin() );
}


//************************************
// Method:    isReaderRegistered
// FullName:  MultiChannelBuffer::isReaderRegistered
// Access:    private 
//************************************

bool MultiChannelBuffer::isReaderRegistered( Reader const & reader ) const
{
    return std::find( readers_.begin(), readers_.end(), &reader ) != readers_.end();
}


//************************************
// Method:    registerReader
// FullName:  MultiChannelBuffer::registerReader
// Access:    private 
//************************************

void MultiChannelBuffer::registerReader( Reader & reader ) const
{
    assert( isInResetState() );
    readers_.push_back( &reader );
}


//************************************
// Method:    reset
// FullName:  MultiChannelBuffer::reset
// Access:    public 
//************************************

void MultiChannelBuffer::reset()
{
    bytesWritten_  = 0;
    writePosition_ = buffer_.begin();
    for( Readers::const_iterator pReader( readers_.begin() ); pReader != readers_.end(); ++pReader )
    {
        Reader & reader( **pReader );
        reader.firstChannelBeginning_   = buffer_.begin();
        reader.availableDataPerChannel_ = 0;
    }
    assert( isInResetState() );
}


//************************************
// Method:    resize
// FullName:  MultiChannelBuffer::resize
// Access:    public 
//************************************

void MultiChannelBuffer::resize( size_t const numberOfChannels, size_t const bytesPerChannel, size_t const maximumExpectedWriteChunk )
{
    buffer_.resize( numberOfChannels * bytesPerChannel );
    channelSize_ = bytesPerChannel;
    assert( numberOfChannels == this->numberOfChannels() );
    minimumSpaceThatMustRemain_ = maximumExpectedWriteChunk;
    reset();
}


//************************************
// Method:    slowestReader
// FullName:  MultiChannelBuffer::slowestReader
// Access:    private 
//************************************

Buffer::const_iterator MultiChannelBuffer::slowestReaderPosition() const
{
    // (todo) recheck this for thread safety.
    Buffer::const_iterator position( (*readers_.begin())->firstChannelBeginning_ );
    for ( Readers::const_iterator pCurrentReader( ++readers_.begin() ); pCurrentReader != readers_.end(); ++pCurrentReader )
    {
        if ( (*pCurrentReader)->firstChannelBeginning_ < position )
            position = (*pCurrentReader)->firstChannelBeginning_;
    }
    return position;
}


//************************************
// Method:    unregisterReader
// FullName:  MultiChannelBuffer::unregisterReader
// Access:    private 
//************************************

void MultiChannelBuffer::unregisterReader( Reader & reader ) const
{
    assert( isReaderRegistered( reader ) );
    readers_.erase( std::find( readers_.begin(), readers_.end(), &reader ) );
}


//************************************
// Method:    writeCompletedCallback
// FullName:  MultiChannelBuffer::writeCompletedCallback
// Access:    public 
//************************************

void MultiChannelBuffer::writeCompletedCallback( size_t const sizeOfWrittenChunk )
{
    writePosition_ += sizeOfWrittenChunk;
    bytesWritten_  += sizeOfWrittenChunk;

    for( Readers::const_iterator pReader( readers_.begin() ); pReader != readers_.end(); ++pReader )
        InterlockedAdd( (**pReader).availableDataPerChannel_, sizeOfWrittenChunk );
}


//************************************
// Method:    Reader
// FullName:  MultiChannelBuffer::Reader::Reader
// Access:    public 
//************************************

MultiChannelBuffer::Reader::Reader( Reader const & source )
    :
    firstChannelBeginning_  ( source.firstChannelBeginning_   ),
    availableDataPerChannel_( source.availableDataPerChannel_ ),
    parent_                 ( source.parent_                  )
{
    parent_.registerReader( *this );
}


//************************************
// Method:    operator++
// FullName:  MultiChannelBuffer::Reader::ChannelIterator::operator++
// Access:    public 
//************************************

MultiChannelBuffer::Reader::ChannelIterator & MultiChannelBuffer::Reader::ChannelIterator::operator++()
{
#if _SECURE_SCL || _HAS_ITERATOR_DEBUGGING
    Buffer const & source( reinterpret_cast<Buffer const &>( *currentBegin_._Getmycont() ) );
    if ( ( source.end() - this->end() ) < static_cast<Buffer::difference_type>( channelSize_ ) )
    {
        currentBegin_ = source.end();
        availableDataPerChannel_ = 0;
    }
    else
#endif
        currentBegin_ += channelSize_;

    return *this;
}


//************************************
// Method:    getChunk
// FullName:  MultiChannelBuffer::Reader::getChunk
// Access:    public 
//************************************

MultiChannelBuffer::Reader::ChannelIterator MultiChannelBuffer::Reader::getChunk( size_t chunkSize )
{
    assert( chunkSize < parent_.channelSize() );
    chunkSize = (std::min)( chunkSize, availableDataPerChannel() );

    ChannelIterator const result( firstChannelBeginning_, chunkSize, parent_.channelSize() );

    // "availableDataPerChannel_ -= chunkSize;":
    InterlockedAdd( availableDataPerChannel_, -static_cast<int>( chunkSize ) );

    // "firstChannelBeginning_ += chunkSize;":
    InterlockedIteratorAdvance( firstChannelBeginning_, chunkSize );

    return result;
}


//************************************
// Method:    WritePointers
// FullName:  MultiChannelBuffer::WritePointers::WritePointers
// Access:    public 
//************************************

MultiChannelBuffer::WritePointers::WritePointers( Buffer::iterator const firstChannelWritePosition, size_t const channelSize, size_t const writeChunkSize, MultiChannelBuffer & parent )
    :
    firstChannelWritePosition_( firstChannelWritePosition ),
    channelSize_              ( channelSize               ),
    writeChunkSize_           ( writeChunkSize            ),
    parent_                   ( parent                    )
{
}


#ifdef _DEBUG // Ugly workaround for the RVO not working in DEBUG builds.
//************************************
// Method:    WritePointers
// FullName:  MultiChannelBuffer::WritePointers::WritePointers
// Access:    public 
//************************************

MultiChannelBuffer::WritePointers::WritePointers( MultiChannelBuffer::WritePointers const & source )
    :
    firstChannelWritePosition_( source.firstChannelWritePosition_ ),
    channelSize_              ( source.channelSize_               ),
    writeChunkSize_           ( source.writeChunkSize_            ),
    parent_                   ( source.parent_                    )
{
    const_cast<size_t &>( source.writeChunkSize_ ) = 0;
}
#endif


//************************************
// Method:    ~WritePointers
// FullName:  MultiChannelBuffer::WritePointers::~WritePointers
// Access:    public 
//************************************

MultiChannelBuffer::WritePointers::~WritePointers()
{
#ifdef _DEBUG // Ugly workaround for the RVO not working in DEBUG builds.
    if ( writeChunkSize_ )
#endif
        parent_.writeCompletedCallback( writeChunkSize_ );
}

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