Juce 6 convolution context.isBypassed operation

I am using convolution to host FIR filters with a bypassButton to toggle context.isBypassed true or false. I leveraged @reuks convolution example in the code below.

void TestAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
   juce::ScopedNoDenormals noDenormals;
   auto totalNumInputChannels = getTotalNumInputChannels();
   auto totalNumOutputChannels = getTotalNumOutputChannels();

   for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
       buffer.clear(i, 0, buffer.getNumSamples());

   const int numChannels = buffer.getNumChannels();
   const int numSamples = buffer.getNumSamples();

   bufferTransfer.get ([this] (BufferWithSampleRate<float>& buf)
           convolution.loadImpulseResponse (std::move (buf.buffer),

   juce::dsp::AudioBlock<float> block (buffer);
   juce::dsp::ProcessContextReplacing<float> context{ block };

   context.isBypassed = bypass;
   convolution.process (context);

Toggling bypass is pretty smooth, but what I am hearing sounds like a short delay (like a 60 ms slap echo) when switching. Listening more closely, it seems when the context switches there is still audio from the previous block that is passing through to the bypassed block and vice versa when switching back and forth

Maybe I am processing this incorrectly? Any assistance appreciated.

Kind regards,

I’ve not encountered this before, but from your description I wonder whether the old and new convolution engines have different latencies. Could you try logging the result of Convolution::getLatency inside your processBlock and see whether the latency changes after switching IRs? If this is the case, you could try using the zero-latency mode and see whether that fixes the issue.

Another possibility is that your IRs may have different early response times (although a difference of 60ms does sound a bit extreme).

@reuk Thanks for taking a look.

I should have mentioned I implemented the trim and normalize functions in app code for filter bookkeeping purposes.

DBG ("Convo latency: " << convolution.getLatency() << " current IR size: " << convolution.getCurrentIRSize());

Convo latency: 0 current IR size: 10530
Convo latency: 0 current IR size: 9778
Convo latency: 0 current IR size: 15063

Are some examples of the filter sizes. Latency is always zero no matter what filter or bypass. With bypass the current IR size remained the same.

I should mention that switching between filters is seamless. It is only when switching between filter and bypass (and vice versa) where the delay is audible.

PS. The filters are somewhat similar in response from a magnitude perspective, but vary in excess phase.

I think the most likely explanation is that the filter kernels themselves are adding some delay, in that case. I’m not sure that there’s a good way to work around this with just the Convolution class. You’d probably need to write your own processor which crossfades between the Convolution (which is kept enabled), and a delayed version of the dry signal.

|              |
v              v
convolution    delay
|              |
v              |
crossfade <----+
1 Like

Thanks. I understand what you are saying.

I did try loading a reverb IR filter. When I engaged the reverb IR and then clicked bypass and then loaded a regular filter, the IR reverb tail is somehow “stored” and now audible as an overlay on top of the newly engaged filter. Like double filtering. Does the explanation account for this scenario?

No, that sounds like it might be a bug. It’s possible that this is broken, although I’d be quite surprised! Is it possible that you reused the same buffer for both IRs, and accidentally left the reverb tail in the buffer when passing the new shorter IR?

1 Like

Thanks, the latter sounds likely. I will check it out and report back.

Yes, this is my error. Thank you for helping me understand!