Side-chaining in SONAR 
by Noel Borthwick
CTO, Cakewalk


Introduction
Side-chaining is a common technique with DSP plug-ins to use a secondary audio input as a modifier of the primary audio input. Some of the more typical uses of side-chaining are with compressors when used for ducking or de-essing.

SONAR has supported side-chaining for both VST and DX plug-ins ever since version 7 of our applications. This article describes how SONAR communicates with side-chain capable VST and DX plug-in's and can also be used as a guide to write a side-chain capable plug-in.

There has been a lot of confusion about VST side-chaining especially since the announcement of VST3. There is a common inaccurate misconception that you need to support VST3 to do side-chaining. Side-chaining in essence is really a very straightforward thing (just requires support for multi-input plug-ins) and it is 100% possible to implement today in VST 2.4. Even better, it requires no custom vendor specific opcodes. Additionally side-chaining has been possible for DirectX plug-ins since the inception of the DX SDK.

This article goes into the implementation details of how SONAR handles side-chaining with VST's as well as DX plug-ins. Note that there is nothing proprietary about this information whatsoever. Many plug-in vendors were using this approach to implement side-chain capabilities even before SONAR supported VST side-chaining.


Side-chaining using VST 2.4

Plug-in Configuration Perspective


To support side-chaining from a plug-in development perspective, all a plug-in needs to do to expose side-chaining capabilities is to support multiple inputs. In VST 2.4, the way this is implemented is by exposing more than one input (as returned in the AEffect struct when the plug-in is loaded) and by supporting the (somewhat counter intuitive) effSetSpeakerArrangement host to plug-in VST 2.4 opcode. This is not specifically called out in the VST documentation to work this way, but most plug-in vendors that support side-chaining have implemented it like this. When implemented this way SONAR will "discover" the extra inputs exposed by the plug-in and list them in the SONAR UI as side-chainable inputs. You can route any track or bus to one of these side-chain inputs.

Besides the DSP code, this is essentially all the plug-in needs to do to support side-chaining in SONAR or any other host that handles this. Also see the host perspective to understand how SONAR communicates with a side-chain capable plug-in.

Host Configuration Perspective

From SONAR's perspective to configure a side-chain capable VST plug-in we initialize the plug-in like this (Note that detail and error checking has been sacrificed in the examples below for readability):


// Begin Code
VSTmainCall fpMain;

// Get VSTMAIN entry point to the plug-in ...

// got the entry point, call it.

struct AEffect* NewVSTEffect = fpMain(EffectMaster);

// Get the number of inputs as exposed by the plug-in
int numInputs = NewVSTEffect->numInputs;

// Clamp to the max used as specified by maxUsedInputs
// Note: the user can specify maxUsedInputs via the Plug-In Manager

numInputs = min(numInputs, maxUsedInputs);

VstInt32 typeIn, typeOut;
VstInt32 numChannelsIn, numChannelsOut;

// If it's a multi-input VST utilize all available inputs.

if ( numInputs > 2 )
{
        typeIn = kSpeakerArrUserDefined;
        numChannelsIn = numInputs;
}

VstSpeakerArrangement* pVstSpeakerArrangementIn;
VstSpeakerArrangement* pVstSpeakerArrangementOut;
int nExtraSpeakers = 0;

// Allocate variable size VstSpeakerArrangement structs
nExtraSpeakers = numChannelsIn > 8 ? numChannelsIn - 8 : 0;
pVstSpeakerArrangementIn
        = (VstSpeakerArrangement*) new BYTE[ sizeof(VstSpeakerArrangement) +
                (nExtraSpeakers * sizeof(VstSpeakerProperties)) ];

nExtraSpeakers = numChannelsOut > 8 ? numChannelsOut - 8 : 0;
pVstSpeakerArrangementOut
        = (VstSpeakerArrangement*) new BYTE[ sizeof(VstSpeakerArrangement) +
                (nExtraSpeakers * sizeof(VstSpeakerProperties)) ];

// Configure the speaker arrangement for the VST to let it know
// which inputs and outputs are in use
// Input speaker arrangement

memset(pVstSpeakerArrangementIn, 0, sizeof VstSpeakerArrangement);

VstSpeakerArrangement& vstSpeakerArrangementIn = *pVstSpeakerArrangementIn;
VstSpeakerArrangement& vstSpeakerArrangementOut = *pVstSpeakerArrangementOut;
vstSpeakerArrangementIn.type = typeIn;
vstSpeakerArrangementIn.numChannels = numChannelsIn;

for (int ix = 0; ix < numChannelsIn; ++ix)
{
        vstSpeakerArrangementIn.speakers[ix].azimuth = 0;
        vstSpeakerArrangementIn.speakers[ix].elevation = 0;
        vstSpeakerArrangementIn.speakers[ix].radius = 0;
        vstSpeakerArrangementIn.speakers[ix].radius = 0;
        vstSpeakerArrangementIn.speakers[ix].type = kSpeakerUndefined;
        vstSpeakerArrangementIn.speakers[ix].name[0] = '\0';
}

// Output speaker arrangement
vstSpeakerArrangementOut.type = typeOut;
vstSpeakerArrangementOut.numChannels = numChannelsOut;

for (int ix = 0; ix < numChannelsOut; ++ix)
{
        vstSpeakerArrangementOut.speakers[ix].azimuth = 0;
        vstSpeakerArrangementOut.speakers[ix].elevation = 0;
        vstSpeakerArrangementOut.speakers[ix].radius = 0;
        vstSpeakerArrangementOut.speakers[ix].radius = 0;
        vstSpeakerArrangementOut.speakers[ix].type = kSpeakerUndefined;
        vstSpeakerArrangementOut.speakers[ix].name[0] = '\0';
}

// Set the input and output speaker arrangement for the plug-in
call_dispatcher(VSTEffect, effSetSpeakerArrangement, 0,
                (LONG_PTR)pVstSpeakerArrangementIn,
                pVstSpeakerArrangementOut, 0.f);

//End code


SONAR specific side-chain Implementation

In SONAR you it's a user decision to use or not use a side-chain input. i.e. side-chain use is not automatic. If the user does not patch a track/bus to a plug-in side-chain input, the side-chain input is unused and is sent a zero filled buffer.

We do not change the input speaker arrangement irrespective of whether the side-chain input is patched from a track/bus or not. i.e. the speaker arrangement is determined by the TOTAL number of inputs reported by the plug-in. We do this for a few reasons. E.g.
  • we don't want to stop and restart the VST when you change the routing in SONAR since it can cause a gap
  • we don't want to change the number of buffers we send to the VST each time routing changes

As a result of this, the VST plug-in does not know if and when tracks are actually routed to the side-chain input from the host side. The plug-in will always receive buffers for ALL exposed inputs, but the side-chain input buffer will be zero filled if nothing is routed to it.

Also note that irrespective of whether SONAR has tracks routed to the side-chain input of the plug-in, it is up to the VST to actually listen to the audio passed to the side-chain input or not. Some plug-ins like the Vintage Channel VC-64, have an explicit button in the GUI to enable side-chaining, when then switches it into actually processing the side-chain input. Also the ability to listen to the side-chain input is typically a property of the plug-in. In VC-64 if you switch to listen mode, the side-chain input gets passed through to the plug-in's primary output without affecting the primary input.

When it's time to send the VST buffers at run time, SONAR calls processReplacing (or processDoubleReplacing), sending it multiple buffers to process - one for each input as reported by the plug-in. Note that SONAR will also send it a buffer for unused side-chain inputs. Unused buffers are always zero filled by the host.

Side-chaining using DirectX


Plug-in Configuration Perspective

From a DX plug-in perspective essentially all that's required to configure a plug-in to be side chain capable is for the plug-in to expose multiple input pins and handle processing multiple inputs. Unlike VST, the DX model has supported this from day one back in 1997 when DirectShow was introduced. Note: The plug-in needs to take thread safety precautions, since the input pin's Receive() method can be called from different threads in SONAR.

Here are some tips for setting up a DX plug-in for side-chaining:

  1. If your filter is derived from CTransformFilter you will need to change the base class of your filter to CBaseFilter because CTransformFilter doesn't support multiple inputs.
  2. The filter should create and maintain an array of input pins. For a side-chain plug-in you will need 2 inputs. The primary input is the zero'th input, the second is the side-chain input.
  3. Return some meaningful name for the sidchain input. We don't use it today but we might in the future.
  4. Each pin now needs to maintain a queue of media samples. You can use an STL deque.
  5. The input pin Receive() method needs to be coded so that it is thread safe while accessing other input pins. Filter input pins can and will be called from multiple threads in SONAR!
  6. The input pin Receive() method should not call Transform to process audio until ALL inputs have been received. What we do for our plug-ins is maintain a count of received inputs for all active input pins.
  7. The filter must be able to deal with only the primary input being connected. SONAR will not connect the side-chain input unless it is actually in use in the project. (connected via a track or bus output). When only the primary input is connected, the filter should not wait for its side-chain input and immediately invoke transform when Receive() is invoked.
  8. In Transform you should processes all inputs that have non-empty receive queues Below are some code examples for reference. Note the locks for thread safety since they are important. Beware of COM reference counting with the input buffers since that's a common error prone area.

// Begin code
// Receive: override to send message to all downstream pins


HRESULT CFxFInputPin::Receive( IMediaSample* pms ) {
        if ( !VERIFY(pms) )
                return E_POINTER;
        CAutoLock     autoLock(m_pLock);

        // Queue up the media sample in our input queue for processing when all inputs are ready 
        ADDREF_MEDIASAMPLE( pms ); 
        m_queueRecv.push_back( pms ); 

        // If we are processing a multi-input plug-in, and multiple input pins have been connected, 
        // we must wait until all inputs have been received, before letting the filter go ahead 
        // with processing the media samples
 
        if ( m_pFilter->MultipleInputsConnected() ) 
        { 
                // Acquire the filter CS 
                CCriticalSection cs( &m_pFilter->m_csState ); 
                // Check if all inputs have been received 
                if (m_pFilter->AllInputsReady()) 
                { 
                        // Process a queued media sample from each input pin 
                        m_pFilter->Transform(NULL);
                 } 
        } 
        else    // standard stereo/mono plug-in or m_bEnableMultipleInputs == FALSE 
        {
        // Don't need to wait for other inputs so go ahead and process the media sample
         m_pFilter->Transform(pms); 
        }
 
        return NOERROR;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Checks if more than one input pin has been connected
// Note we assume that the primary input is always connected

BOOL CFxFilter::MultipleInputsConnected()

        UINT cInputs = (UINT)m_apInputPins.size(); 
        for (UINT i = 1; i < cInputs; i++) 
        { 
                if (m_apInputPins[i]->IsConnected()) 
                        return TRUE;
        } 
        return FALSE;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Checks if all connected pin's input queues have at least one ready media sample


BOOL CFxFilter::AllInputsReady()

        UINT cInputs = (UINT)m_apInputPins.size(); 
        for (UINT i = 0; i < cInputs; i++) 
        { 
                if (m_apInputPins[i]->IsConnected() && m_apInputPins[i]->m_queueRecv.empty())
                        return FALSE; 
        } 
        return TRUE;
}

// Returns true if the pin is connected. false otherwise.
BOOL CFxFilter::IsConnected(void) {return (m_Connected != NULL); };

// =================================================================
// Process a media sample supplied by the input pin(s) and produce
// output by calling Deliver on all output pins.
//
// NOTE: It is legal for the pSample argument to be NULL when the filter
// has multiple input pins. In this scenario we must deque a MS from each
// input queue into the output buffer prior to processing.
// =================================================================


HRESULT CFxFilter::Transform(IMediaSample *pSample)

        CCriticalSection cs( &m_csState ); 

        // If an input MS was not supplied, use the MS from the primary input pin. 
        // We assume that the buffer size will be the same across all inputs! 

        if ( pSample == NULL )
        { 
                CFxFInputPin* pPinPrimary = m_apInputPins[0]; 
                if ( !pPinPrimary->m_queueRecv.empty() )
                { 
                        deque<IMediaSample*>::iterator it = pPinPrimary->m_queueRecv.begin(); 
                        pSample = *it; 
                } 
                if ( !VERIFY(pSample) ) return E_POINTER; 
        } 
        int cbd = pSample->GetActualDataLength(); 

        // Do the actual work of processing all input buffers 
        // This processes all inputs with nonempty receive queues (m_queueRecv) 

        DoActualTransform(cbd, pSample); 
        return S_OK;
}

// End code


Host Configuration Perspective

SONAR communicates to a side-chain capable DX plug-in very similarly to how works with other DirectX plug-ins. One difference is in the connection process. SONAR will not connect the side-chain input unless it is actually in use in the project. i.e. connected via a track or bus that sends to this side-chain input. As a result of this the filter must be able to deal with only the primary input being connected. Also as mentioned earlier beware of thread safety issues while implementing your input pins. Filter input pins can and will be called from multiple threads in SONAR!

Examples of VST 2.4 side-chain capable plug-ins

A few examples of VST 2.4 plug-ins that are side-chain capable:
Vintage Channel (VC-64)
Sold as part of the Cakewalk SONAR product suite
http://www.cakewalk.com/Products/DAWs.asp

Voxengo Crunchessor
http://www.voxengo.com/product/crunchessor/

Sonalksis SV-315 Mk2 Compressor and SV-719 Analogue Gate
http://www.sonalksis.com/index.php

OtiumFX Compadre
http://www.otiumfx.com/compadre.php

Examples of DirectX side-chain capable plug-ins

Sonitus Compressor
http://www.cakewalk.com/Products/Sonitus/sonitus1.asp