Custom low pass filter using OnAudioFilterRead

I’m looking to apply a low pass filter to some audio sources, but we cannot use the built in Audio Low Pass Filter, since we want to be able to apply the low pass to specific channels. So it looks like the best option would be to write a custom low pass filter in OnAudioFilterRead that directly applies the output to the data.

I’m very new to audio programming, and I’m looking for a resource to help with this. Low pass filters take in a cutoff frequency and use that to attenuate values above the frequency (leaving those below unaltered), but I’m not sure how to properly convert the PCM amplitude data that is passed into OnAudioFilterRead into frequencies (I assume it involves sampling adjacent amplitudes?). Wondering if anyone knows how the built in Audio Low Pass is implemented (the decompiled component only has external calls :()

Thanks for any help, and feel free to correct any incorrect assumptions I’ve made above!

1 Like

Found this answer on StackOverflow with some code, plugged it in and…got it working!

using UnityEngine;

public class ExperimentMuffle : MonoBehaviour
{
    /// <summary>
    /// Array of input values, latest are in front
    /// </summary>
    private float[] inputHistory = new float[2];

    /// <summary>
    /// Array of output values, latest are in front
    /// </summary>
    private float[] outputHistory = new float[3];

    private float c, a1, a2, a3, b1, b2;

    [SerializeField, Range(10, 22200)]
    int cutoffFrequency = 1000;

    [SerializeField]
    float lowpassResonanceQ = 1;

    private void OnValidate()
    {
        c = 1.0f / (float)Mathf.Tan(Mathf.PI * cutoffFrequency / AudioSettings.GetConfiguration().sampleRate);
        a1 = 1.0f / (1.0f + lowpassResonanceQ * c + c * c);
        a2 = 2f * a1;
        a3 = a1;
        b1 = 2.0f * (1.0f - c * c) * a1;
        b2 = (1.0f - lowpassResonanceQ * c + c * c) * a1;

        // Clear the history to avoid introducing errors due to settings changes.
        inputHistory[1] = 0;
        inputHistory[0] = 0;

        outputHistory[2] = 0;
        outputHistory[1] = 0;
        outputHistory[0] = 0;
    }

    private void Awake()
    {
        OnValidate();
    }

    private void OnAudioFilterRead(float[] data, int channels)
    {      
        for (int i = 0; i < data.Length; i += 2)
        {
            data[i] = AddInput(data[i]);
        }
    }

    private float AddInput(float newInput)
    {
        float newOutput = a1 * newInput + a2 * inputHistory[0] + a3 * inputHistory[1] - b1 * outputHistory[0] - b2 * outputHistory[1];

        inputHistory[1] = inputHistory[0];
        inputHistory[0] = newInput;

        outputHistory[2] = outputHistory[1];
        outputHistory[1] = outputHistory[0];
        outputHistory[0] = newOutput;

        return outputHistory[0];
    }
}

Comparing against the Unity one sounds pretty much the same. Not entirely sure how it all works, will have to dig into it a bit more.

1 Like

I ran into a similar issue.
Thank you for the rewritten code you provided.
I made myself two functions HighPassFilter() and LowPassFilter(), which can be used when working with AudioClip.GetData()
When we have AudioClip and we make changes, then at one stage we need to remove the low frequencies, in this case we can call the HighPassFilter() function and pass it the parameters of your sound with all the data:

    [HideInInspector]
    private float c, a1, a2, a3, b1, b2;
    public void HighPassFilter (float[] waveData, int channels, int audioSampleFrequency, int cutoffFrequency) {
        c = Mathf.Tan (Mathf.PI * cutoffFrequency / audioSampleFrequency);
        a1 = 1f / (1f + lowpassResonanceQ * c + c * c);
        a2 = -2 * a1;
        a3 = a1;
        b1 = 2f * (c * c - 1f) * a1;
        b2 = (1f - lowpassResonanceQ * c + c * c) * a1;
        inputHistory[1] = 0;
        inputHistory[0] = 0;
        outputHistory[2] = 0;
        outputHistory[1] = 0;
        outputHistory[0] = 0;
        //for 2-channel audio first 4 floats would be L[0], R[0], L[1], R[1]
        for (int i = channels - 1; i < waveData.Length; i += channels) {
            waveData[i] = AddInput (waveData[i]);
        }
        if (channels == 1) {
            return;
        }
        for (int i = channels - 2; i < waveData.Length; i += channels) {
            waveData[i] = AddInput (waveData[i]);
        }
    }
    public void LowPassFilter (float[] waveData, int channels, int audioSampleFrequency, int cutoffFrequency) {
        c = 1f / (float)Mathf.Tan (Mathf.PI * cutoffFrequency / audioSampleFrequency);
        a1 = 1f / (1f + lowpassResonanceQ * c + c * c);
        a2 = 2f * a1;
        a3 = a1;
        b1 = 2f * (1f - c * c) * a1;
        b2 = (1f - lowpassResonanceQ * c + c * c) * a1;
        inputHistory[1] = 0;
        inputHistory[0] = 0;
        outputHistory[2] = 0;
        outputHistory[1] = 0;
        outputHistory[0] = 0;
        //for 2-channel audio first 4 floats would be L[0], R[0], L[1], R[1]
        for (int i = channels - 1; i < waveData.Length; i += channels) {
            waveData[i] = AddInput (waveData[i]);
        }
        if (channels == 1) {
            return;
        }
        for (int i = channels - 2; i < waveData.Length; i += channels) {
            waveData[i] = AddInput (waveData[i]);
        }
    }
    [HideInInspector]
    private float[] inputHistory = new float[2];
    [HideInInspector]
    private float[] outputHistory = new float[3];
    private float AddInput (float newInput) {
        float newOutput = a1 * newInput + a2 * inputHistory[0] + a3 * inputHistory[1] - b1 * outputHistory[0] - b2 * outputHistory[1];
        inputHistory[1] = inputHistory[0];
        inputHistory[0] = newInput;
        outputHistory[2] = outputHistory[1];
        outputHistory[1] = outputHistory[0];
        outputHistory[0] = newOutput;
        return outputHistory[0];
    }