Sidechain and ProcessContextReplacing

Hi guys,
I’m having some problems with a plugin that needs a sidechain input. The plugin can be loaded either as mono or stereo.

I enabled the sidechain this way:

AudioProcessor (BusesProperties().withInput  ("Input",  AudioChannelSet::stereo(), true)
                                   .withOutput ("Output", AudioChannelSet::stereo(), true)
                                   .withInput ("Sidechain", AudioChannelSet::stereo(), true))

Now, since I’m using dsp::Oversampling, I use dsp::ProcessContextReplacing for the actual process and it’s called on each processBlock cycle. Since I need to pass a block containing both the main and the sidechain buses, I use a 4 channel buffer that I set this way:

(from processBlock)

    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();
    
    //get buffers from inputs
    auto mainInputOutput = getBusBuffer (buffer, true, 0);
    auto sideChainInput  = getBusBuffer (buffer, true, 1);
    
    auto numSamples = buffer.getNumSamples();
    
    /*
     QUAD CHANNEL BUFFER FOR SIDECHAIN
     Chan 0-1: main input/output
     Chan 2-3: sidechain input/ouput
     */
    quadBuffer.copyFrom(0, 0, mainInputOutput, 0, 0, numSamples);
    quadBuffer.copyFrom(1, 0, mainInputOutput, (mainInputOutput.getNumChannels() > 1 ? 1 : 0), 0, numSamples);

if(useSideChain && sideChainInput.getNumChannels() > 0)
{
    quadBuffer.copyFrom(2, 0, sideChainInput, 0, 0, numSamples);
    quadBuffer.copyFrom(3, 0, sideChainInput, (sideChainInput.getNumChannels() > 1 ? 1 : 0), 0, numSamples);
}
else
{
    quadBuffer.copyFrom(2, 0, mainInputOutput, 0, 0, numSamples);
    quadBuffer.copyFrom(3, 0, mainInputOutput, (mainInputOutput.getNumChannels() > 1 ? 1 : 0), 0, numSamples);
}

The Oversampler is set to process up to 4 channels.

PROBLEM:
The sidechain seems to work, but as soon as Input Monitoring is enabled on any DAW, the sound shows a lot of glitches and “screams”. Disabling the Input Monitoring leads, then, to no audio coming out the plugin unless you don’t bypass it from the DAW and enable it again.

I suspected something went wrong with the quadBuffer above, but I tried forcing the process to a single channel only, without dealing with the sidechain channel and I get the same results.

If I move the processing away from ProcessContextReplacing back to processBlock (so dealing with the buffers instead of the AudioBlocks), everything works fine but I would lose the oversampling, since I rely on the dsp::Oversampler class.

Any advice?

Thanks

Original thread updated with new findings.

On the first glance, the code snippet above looks fine. But how do you continue from there? Would you like to post how you use the quadBuffer, create the block from it, create the process context and use the oversampler? I would assume that the error comes up later in your processing code.

The way I’d expect your code to continue would be e.g. (completely untested, just written down from memory)

 // directly pass in the buffer makes use of the implicit AudioBuffer to AudioBlock conversion
auto oversampledQuadBlock = oversampler->processSamplesUp (quadBuffer);

// You can work on that block with a processContextReplacing if using dsp module processors.
// Let's assume you have a processor chain holding some processors
chain.process<juce::dsp::ProcessContextReplacing<float>> (oversampledQuadBlock);

// when downsampling the four channel block again you need a four channel destination buffer, even if
// you are only going to use the first two channels as output. Directly writing to the processing buffer 
// would be a bad idea since you would overwrite your read-only side chain signal
oversampler->processSamplesDown (quadBuffer);

for (int ch = 0; ch < totalNumOutputChannels; ++ch)
    buffer.copyFrom (ch, 0, quadBuffer, 0, 0, numSamples);

That’s what I’m doing:

in processBlock I pass the quadBuffer as a block:

dsp::AudioBlock<float> block (quadBuffer); 

if (isStereo)
    {
        // Stereo processing mode:
       process (dsp::ProcessContextReplacing<float> (block));
        
        buffer.copyFrom(0, 0, quadBuffer, 0, 0, numSamples);
        buffer.copyFrom(1, 0, quadBuffer, 1, 0, numSamples);
}
else
{
        auto newBlock = block.getSubsetChannelBlock(1, 2);
        process (dsp::ProcessContextReplacing<float> (newBlock));
        
        buffer.copyFrom(0, 0, quadBuffer, 1, 0, numSamples);
}

While in ProcessContextReplacing I have:

void myAudioProcessor::process (dsp::ProcessContextReplacing<float> context) noexcept
{
    ScopedNoDenormals noDenormals;
    
    auto&& inBlock  = context.getInputBlock();
    auto&& outBlock = context.getOutputBlock();
    
    jassert (inBlock.getNumChannels() == outBlock.getNumChannels());
    jassert (inBlock.getNumSamples() == outBlock.getNumSamples());
    
    auto numSamples  = inBlock.getNumSamples();
    auto numChannelsTotal = inBlock.getNumChannels();
    
    // Upsampling
    dsp::AudioBlock<float> oversampledBlock;
    
    setLatencySamples (audioCurrentlyOversampled ? roundToInt (oversampling->getLatencyInSamples()) : 0);
    
    // Upsampling
    if (audioCurrentlyOversampled)
    {
        oversampledBlock = oversampling->processSamplesUp (context.getInputBlock());
    }
    
    auto pluginContext = audioCurrentlyOversampled ? dsp::ProcessContextReplacing<float> (oversampledBlock)
    : context;
    
    
    auto&& plugInBlock  = pluginContext.getInputBlock();
    auto&& plugOutBlock = pluginContext.getOutputBlock();
    
    jassert (plugInBlock.getNumChannels() == plugOutBlock.getNumChannels());
    jassert (plugInBlock.getNumSamples() == plugOutBlock.getNumSamples());
    
    numSamples  = plugInBlock.getNumSamples();
    numChannelsTotal = plugInBlock.getNumChannels();
    
    const int numChannels = numChannelsTotal > 2 ? 2 : 1;
    const int chanInc = numChannels > 1 ? 2 : 1;
    
    //Plugin Processing
    for (size_t chan = 0; chan < numChannels; ++chan)
    {
        for (size_t i = 0; i < numSamples; ++i)
        {
            const float dry = plugInBlock.getChannelPointer(chan)[i];
            const float scInput = useSideChain ? plugInBlock.getChannelPointer(chan+chanInc)[i] : 0.0f;
            
            const float fxOut = useSideChain ? myFX[chan].processTick(dry, scInput) : myFX[chan].processTick(nrDry);
            plugOutBlock.getChannelPointer(chan)[i] = fxOut;
            
            jassert (!std::isnan (plugOutBlock.getChannelPointer(chan)[i]));
        }
    }
    
    // Downsampling
    if (audioCurrentlyOversampled)
    {
        oversampling->processSamplesDown (context.getOutputBlock());
    }
}

That’s it. I tried the very same approach in processBlock, working with buffers (like I always did before the dsp module where introduced) and everything works as expected.

Thanks!

UPDATE: I got it working bypassing ProcessContextReplacing and working in processBlock, using the AudioBlock only to deal with the oversampler. Cumbersome and redundant, in my opinion, but it works.