Dealing with aliasing when (un)bypassing effects and other processes

Hi,

I’m struggling with an elegant solution to a, I assume, common problem. I’m working on a synthesizer which features different filter models. The user can switch to a different model on the fly. When doing so, inevitably some aliassing will occur because the signal changes immediately. Meaning, the signal out of filter A is different from the signal out of filter B, thus when immediately switching some aliasing will occur.

To mitigate this I’ve created a simple state machine that fades out and in when a change of selection has occured.
It works as follows: user selects a different model than the current. At this point the state is set to ‘FADE_OUT’. The old model is still applied and a .applyGainRamp is applied to the buffer until the gain reaches 0.0. Then, state is changed to ‘FADE_IN’ and the new model will be used. applyGainRamp is applied with an increasing gain over time until gain reaches 1.0. State is reset to ‘NO_FADE’.

See this:

template <typename FloatType>
void  MonosynthPluginAudioProcessor::processFilterBlending(AudioBuffer<FloatType>& buffer)
{

    int numSamples = buffer.getNumSamples();
    int blendTimeSamples = sampleRate * 0.01;
    FloatType gainRampCoeff = ( (FloatType)numSamples / (FloatType)blendTimeSamples);

    if (lastFilterChoice != *filterSelectParam)
    {
         if (filterBlendState == NO_FADE) filterBlendState = FADE_OUT;
    }

    // APPLY FILTER
    LadderFilterBase* curFilter;

    switch (lastFilterChoice) {
        case 0:
            curFilter = filterA.get();
            break;
        case 1:
            curFilter = filterB.get();
            break;
        case 2:
            curFilter = filterC.get();
            break;
    }

    applyFilterEnvelope(buffer, curFilter);
    applyFilter(buffer, curFilter);

     switch (filterBlendState)
     {
        case FADE_OUT : {
            FloatType beginGain = filterGain;
            filterGain -= gainRampCoeff;
            buffer.applyGainRamp(0, 0, numSamples, beginGain, filterGain);
        
            if (filterGain <= 0.0) {
                lastFilterChoice = *filterSelectParam;
                filterBlendState = FADE_IN;
            }
        
            return;
        }
        case FADE_IN: {
            FloatType beginGain = filterGain;
            filterGain += gainRampCoeff;
            buffer.applyGainRamp(0, 0, numSamples, beginGain, filterGain);
        
            if (filterGain >= 1.0) {
                filterBlendState = NO_FADE;
                filterGain = 1.0;
            }
            return;
        }
        case NO_FADE: {
            return;
        }
    }
}

But this solution is not perfect. On other effects such as a chorus, aliasing is still very noticable, except of course when I increase the fade in/out time.

So my question is, are there any other techniques that I could apply to solve this problem?

Ideally you would process your block with both filters, but you need a copy of your original for that:
This is pseudo code:

if (newFilter != lastFilter)
{
    spareBuffer.copyFrom (buffer, ...);
    buffer.apply (lastFilter);
    buffer.applyGainRamp (1.0, 0.0);
    spareBuffer.apply (newFilter);
    spareBuffer.applyGainRamp (0.0, 1.0);
    buffer.addFrom (spareBuffer);
    lastFilter = newFilter;
}
else
{
    buffer.apply (newFilter);
}

One of many solutions

Thanks for your reply! That solution looks much simpler.

I’ve tried crossfading initially but I got weird artifacting when the cutoff was at lower values. Probably has to do with the fact that my envelope generator was applied twice (once for the old filter and once for the new) so it got out of sync with the rest.

This works like a charm:

int numSamples = buffer.getNumSamples();
LadderFilterBase* curFilter;

if (lastFilterChoice != *filterSelectParam)
{
	int newFilterChoice = *filterSelectParam;

	AudioBuffer<FloatType> tempBuffer;
	tempBuffer.makeCopyOf(buffer);

	int blendTimeSamples = (sampleRate / oversampleFactor) * 0.04; //40 milliseconds
	FloatType gainRampCoeff = ((FloatType)numSamples / (FloatType)blendTimeSamples);

	FloatType beginGain = filterGain;
	filterGain -= gainRampCoeff;

	switch (lastFilterChoice) {
	case 0:
		curFilter = filterA.get();
		break;
	case 1:
		curFilter = filterB.get();
		break;
	case 2:
		curFilter = filterC.get();
		break;
	}

	applyFilterEnvelope(buffer, curFilter);
	applyFilter(buffer, curFilter);

	buffer.applyGainRamp(0, 0, numSamples, beginGain, filterGain);

	switch (newFilterChoice) {
	case 0:
		curFilter = filterA.get();
		break;
	case 1:
		curFilter = filterB.get();
		break;
	case 2:
		curFilter = filterC.get();
		break;
	}

	applyFilterEnvelope(tempBuffer, curFilter);
	applyFilter(tempBuffer, curFilter);

	tempBuffer.applyGainRamp(0, 0, numSamples, 1.0 - beginGain, 1.0 - filterGain);

	buffer.addFrom(0,0,tempBuffer, 0, 0, numSamples);

	if (filterGain <= 0.0)
	{
		lastFilterChoice = newFilterChoice;
		filterGain = 1.0;
	}
}
else
{

	switch (lastFilterChoice) {
	case 0:
		curFilter = filterA.get();
		break;
	case 1:
		curFilter = filterB.get();
		break;
	case 2:
		curFilter = filterC.get();
		break;
	}

	applyFilterEnvelope(buffer, curFilter);
	applyFilter(buffer, curFilter);
}

except that makeCopyOf allocates memory in your processBlock… The better approach is to allocate in prepareToPlay and use tempBuffer.copyFrom afterwards…

Yeah, I imagined so I just have to correct that.