RustAudio / rodio

Rust audio playback library
Apache License 2.0
1.72k stars 222 forks source link

Support for more equalizer filters #331

Open jiminycrick opened 3 years ago

jiminycrick commented 3 years ago

For the project I am working on we would like to EQ some sound effects. Rodio has a low pass filter (which is labelled as buggy in the docs but we haven't used it yet so I can't comment). A high pass filter, a band pass, and a band reject would be very useful. Even if these never get implemented, thank you for working on Rodio, it is a great project and very helpful!

tuzz commented 3 years ago

Slightly unrelated, but I noticed the low pass filter produces Infinity/NaN values if the frequency is set to (just below) half the sampling rate of the source, or higher. This causes the audio to cut out or to stutter.

You can see this for yourself if you play a 32 kHz audio file with source.low_pass(16_000) applied. I think this is because of Nyquist rate but I'm not sure.

Additionally, the low pass filter downmixes to mono.

For my current project, I ended up writing my own LowPassSource based on the one in rodio that addresses these problems. Some of it's project specific but feel free to reference it if someone wants to have a go at improving the one in the rodio crate. Thanks

low_pass_source.rs

use crate::*;

// This is based on the implementation from the rodio crate:
// https://github.com/RustAudio/rodio/blob/a6f50364b0dbe8869519b2e79c65c4a606aadccd/src/source/blt.rs
//
// It differs in that it works in stereo, uses precomputed coefficients and
// allows the threshold value to change over time via an Arc.

#[derive(Clone)]
pub struct LowPassSource<S: Source> where S::Item: Sample {
    stereo: S,
    threshold_frequency: Arc<AtomicU32>,

    previous: [(f32, f32, f32, f32); 2],
    channel_index: usize,
}

impl<S: Source> LowPassSource<S> where S::Item: Sample {
    pub fn new(stereo: S, threshold_frequency: Arc<AtomicU32>) -> Self {
        assert_eq!(stereo.channels(), 2);

        let previous = [(0., 0., 0., 0.), (0., 0., 0., 0.)];
        Self { stereo, threshold_frequency, previous, channel_index: 1 }
    }
}

impl<I> Iterator for LowPassSource<I> where I: Source<Item=f32> {
    type Item = f32;

    fn next(&mut self) -> Option<Self::Item> {
        self.channel_index = 1 - self.channel_index;
        let sample = self.stereo.next()?;

        let threshold_frequency = self.threshold_frequency.load(Relaxed) as usize;
        if threshold_frequency >= MAX_LOW_PASS_FREQUENCY { return Some(sample); }

        let sample_rate = self.stereo.sample_rate();

        let (b0, b1, b2, a1, a2) = LOW_PASS_COEFFICIENTS[&sample_rate][threshold_frequency];
        let (x_n1, x_n2, y_n1, y_n2) = &mut self.previous[self.channel_index];

        let result = b0 * sample + b1 * *x_n1 + b2 * *x_n2 - a1 * *y_n1 - a2 * *y_n2;

        *y_n2 = *y_n1;
        *x_n2 = *x_n1;
        *y_n1 = result;
        *x_n1 = sample;

        Some(result)
    }
}

impl<I> Source for LowPassSource<I> where I: Source<Item=f32> {
    fn current_frame_len(&self) -> Option<usize> { self.stereo.current_frame_len() }
    fn channels(&self) -> u16                    { self.stereo.channels() }
    fn sample_rate(&self) -> u32                 { self.stereo.sample_rate() }
    fn total_duration(&self) -> Option<Duration> { self.stereo.total_duration() }
}

low_pass_coefficients.rs

use crate::*;

// Precomputes all combinations of (sample_rate, threshold_frequency) so that
// coefficients used for low pass filtering can be looked up efficiently.

lazy_static! {
    pub static ref LOW_PASS_COEFFICIENTS: LowPassCoefficients = precompute_coefficients_for_low_pass_filtering();
}

type LowPassCoefficients = HashMap<SampleRate, Vec<Coefficients>>;
type SampleRate = u32;
type Coefficients = (f32, f32, f32, f32, f32);

pub const MAX_LOW_PASS_FREQUENCY: usize = 15_990;

fn precompute_coefficients_for_low_pass_filtering() -> LowPassCoefficients {
    let sample_rates = SOUNDS.iter().map(|s| s.source.sample_rate()).collect::<HashSet<u32>>();
    let mut precomputed = HashMap::default();

    for sample_rate in sample_rates {
        // LowPassSource diverges to infinite values if the threshold is greater
        // than ~half the sampling rate, probably something to do with Nyquist rate.
        // Therefore, check sounds have a high enough sampling rate for the const.
        assert!(sample_rate / 2 > MAX_LOW_PASS_FREQUENCY as u32);

        let mut vec = Vec::with_capacity(MAX_LOW_PASS_FREQUENCY);

        for threshold_frequency in 0..MAX_LOW_PASS_FREQUENCY {
            let w0 = 2.0 * PI * threshold_frequency as f32 / sample_rate as f32;
            let q = 0.5;

            let alpha = w0.sin() / (2.0 * q);
            let b1 = 1.0 - w0.cos();
            let b0 = b1 / 2.0;
            let b2 = b0;
            let a0 = 1.0 + alpha;
            let a1 = -2.0 * w0.cos();
            let a2 = 1.0 - alpha;

            let coefficients = (b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0);
            vec.push(coefficients);
        }

        precomputed.insert(sample_rate, vec);
    }

    precomputed
}