Search code examples
c#naudio

Audible "popping" while applying a BiQuad low pass filter to an audio stream using NAudio


Context: I'm a software engineer with very little exposure into the core principles of audio engineering. I have an open source plugin that plays audio in response to application & user behavior. One feature I've been working on is to apply a low-pass filter to music based on user settings to give the music a 'muffled' effect. For example, the music could become 'muffled' when entering the settings of the application. My plugin relies on the audio library NAudio to perform most of its audio-related operations.

Problem: When the audio is transitioning into & off of the filter, its very common for there to be a jarring "popping" sound. Does anyone know a clear solution to this "popping" effect?

Most resources I've read indicate too harsh of a transition, but nothing I tried on this front seems to have resolved the issue.

I made a reddit post & tried the solutions offered there. There's a walk-through of the original logic there for those interested. The class responsible for this behavior can be found here.

I've tried fading the filter from/to 20khz with the popping simply cropping in toward the end of the fade, what I'm assuming is the more audible range.

As suggested by one user in the reddit post, I attempted to create two frequency groups, each residing on each side of the filter threshold & then mix them back into the audio buffer. As time progresses, one group is faded out & the other is faded in, depending if the 'muffle' effect is enabled.

I established the low & high filters for each group:

_lowPassFilters = new BiQuadFilter[provider.WaveFormat.Channels];
_highPassFilters = new BiQuadFilter[provider.WaveFormat.Channels];
for (var i = 0; i < provider.WaveFormat.Channels; i++)
{
    _lowPassFilters[i] = BiQuadFilter.LowPassFilter(
        WaveFormat.SampleRate, muffledLowerBound, muffledBandwidth);
    _highPassFilters[i] = BiQuadFilter.HighPassFilter(
        WaveFormat.SampleRate, muffledUpperBound, muffledBandwidth);
}

The code responsible for calculating the cut-off/threshold frequency & volume for each sample on fade in:

var fadedCutOff = _upperBound - _muffleCutOffFrequencyIncrement * _muffleFadeSamplePosition;
var lowFilterVolume = _muffleFadeSamplePosition / (float)_muffleFadeSampleCount;
ApplyFadedMuffle(buffer, offset, fadedCutOff, lowFilterVolume, ref sampleIndex);

The code responsible for calculating the cut-off/threshold frequency & volume for each sample on fade out:

var fadedCutOff = _lowerBound + _muffleCutOffFrequencyIncrement * _muffleFadeSamplePosition;
var lowFilterVolume = 1 - _muffleFadeSamplePosition / (float)_muffleFadeSampleCount;
ApplyFadedMuffle(buffer, offset, fadedCutOff, lowFilterVolume, ref sampleIndex);

And the method that applies the filters to the audio stream:

private void ApplyFadedMuffle(float[] buffer, int offset, float fadedCutOff, float lowPassVolume, ref int sampleIndex)
{
    var highPassVolume = 1 - lowPassVolume;
    for (var i = 0; i < WaveFormat.Channels; i++)
    {
        var lowFilter = _lowPassFilters[i];
        var highFilter = _highPassFilters[i];

        lowFilter.SetLowPassFilter(WaveFormat.SampleRate, fadedCutOff, _bandwidth);
        highFilter.SetHighPassFilter(WaveFormat.SampleRate, fadedCutOff, _bandwidth);

        var bufferIndex = offset + sampleIndex++;
        var value = buffer[bufferIndex];
        var lowPassValue = lowFilter.Transform(value) * lowPassVolume;
        var highPassValue = highFilter.Transform(value) * highPassVolume;
        buffer[bufferIndex] = lowPassValue + highPassValue;
    }
}

I also tried a variation of the above method where instead of having a high filter applied, I simply mix in the original audio sample multiplied by the the same volume as before:

var highPassValue = value * highPassVolume;

I still experienced the same popping with the above solutions, with some minor audio distortions added during the fade.


Solution

  • While "fading in" the muffle, I was applying the volume, then the filter. Once fully faded in, I reversed & applied the filter, then the volume.

    Switching the order when fully applied fixed the issue.