|
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.
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:
// 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*> 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 |
|||
|
|
|||