rill-lang 0.6.0-M2

rill-lang — a Faust-style functional streaming DSL compiled to rill Algorithm<T>
Documentation
use rill_core::math::Transcendental;

/// Biquad filter type for EQ bands.
pub enum BandType {
    /// Peaking/parametric bell filter.
    Peak = 0,
    /// Low shelf filter.
    LowShelf = 1,
    /// High shelf filter.
    HighShelf = 2,
    /// Second-order lowpass.
    LowPass = 3,
    /// Second-order highpass.
    HighPass = 4,
    /// Bandpass filter (constant skirt gain, peak at unity).
    BandPass = 5,
    /// Notch/band-reject filter.
    Notch = 6,
}

/// Configuration for a single EQ band.
pub struct EqBandConfig {
    /// Center/corner frequency in Hz.
    pub freq: f64,
    /// Q factor (bandwidth).
    pub q: f64,
    /// Gain in dB (ignored for pass/notch filters).
    pub gain_db: f64,
    /// Filter type.
    pub band_type: BandType,
}

/// Configuration for a full parametric EQ (cascaded biquad bands).
pub struct EqConfig {
    /// Ordered list of EQ bands (processed sequentially).
    pub bands: Vec<EqBandConfig>,
}

struct BiquadCoeffs {
    b0: f64,
    b1: f64,
    b2: f64,
    a1: f64,
    a2: f64,
}

/// Runtime state for a cascaded biquad EQ.
pub struct EqState<T: Transcendental> {
    config: EqConfig,
    coeffs: Vec<BiquadCoeffs>,
    x1: Vec<T>,
    x2: Vec<T>,
    y1: Vec<T>,
    y2: Vec<T>,
    sample_rate: f32,
}

impl<T: Transcendental> EqState<T> {
    /// Create EQ state from config, computing biquad coefficients at the given sample rate.
    pub fn new(config: EqConfig, sample_rate: f32) -> Self {
        let n = config.bands.len();
        let mut state = Self {
            coeffs: Vec::with_capacity(n),
            x1: vec![T::ZERO; n],
            x2: vec![T::ZERO; n],
            y1: vec![T::ZERO; n],
            y2: vec![T::ZERO; n],
            config,
            sample_rate,
        };
        state.recompute_coeffs();
        state
    }

    /// Process a single sample through all EQ bands (cascaded).
    pub fn process_sample(&mut self, input: T) -> T {
        let mut x = input;
        for i in 0..self.config.bands.len() {
            let c = &self.coeffs[i];
            let b0 = T::from_f64(c.b0);
            let b1 = T::from_f64(c.b1);
            let b2 = T::from_f64(c.b2);
            let a1 = T::from_f64(c.a1);
            let a2 = T::from_f64(c.a2);
            let y = b0 * x + b1 * self.x1[i] + b2 * self.x2[i] - a1 * self.y1[i] - a2 * self.y2[i];
            self.x2[i] = self.x1[i];
            self.x1[i] = x;
            self.y2[i] = self.y1[i];
            self.y1[i] = y;
            x = y;
        }
        x
    }

    fn recompute_coeffs(&mut self) {
        self.coeffs.clear();
        for band in &self.config.bands {
            self.coeffs
                .push(compute_biquad(band, self.sample_rate as f64));
        }
    }

    /// Process an entire buffer of samples through the EQ.
    pub fn process_slice(&mut self, input: &[T], output: &mut [T]) {
        for (i, sample) in input.iter().enumerate() {
            output[i] = self.process_sample(*sample);
        }
    }
}

fn compute_biquad(band: &EqBandConfig, sr: f64) -> BiquadCoeffs {
    use std::f64::consts::PI;
    let freq = band.freq.max(20.0).min(sr * 0.49);
    let omega = 2.0 * PI * freq / sr;
    let sn = omega.sin();
    let cs = omega.cos();
    let alpha = sn / (2.0 * band.q.max(0.1));

    let (b0, b1, b2, a0, a1, a2) = match band.band_type {
        BandType::LowPass => {
            let b1 = 1.0 - cs;
            (b1 / 2.0, b1, b1 / 2.0, 1.0 + alpha, -2.0 * cs, 1.0 - alpha)
        }
        BandType::HighPass => {
            let b1 = 1.0 + cs;
            (b1 / 2.0, -b1, b1 / 2.0, 1.0 + alpha, -2.0 * cs, 1.0 - alpha)
        }
        BandType::BandPass => (
            sn / 2.0,
            0.0,
            -sn / 2.0,
            1.0 + alpha,
            -2.0 * cs,
            1.0 - alpha,
        ),
        BandType::Notch => (1.0, -2.0 * cs, 1.0, 1.0 + alpha, -2.0 * cs, 1.0 - alpha),
        BandType::Peak => {
            let a = 10.0f64.powf(band.gain_db / 40.0);
            (
                1.0 + alpha * a,
                -2.0 * cs,
                1.0 - alpha * a,
                1.0 + alpha / a,
                -2.0 * cs,
                1.0 - alpha / a,
            )
        }
        BandType::LowShelf => {
            let a = 10.0f64.powf(band.gain_db / 40.0);
            let sa = 2.0 * a.sqrt() * alpha;
            (
                a * ((a + 1.0) - (a - 1.0) * cs + sa),
                2.0 * a * ((a - 1.0) - (a + 1.0) * cs),
                a * ((a + 1.0) - (a - 1.0) * cs - sa),
                (a + 1.0) + (a - 1.0) * cs + sa,
                -2.0 * ((a - 1.0) + (a + 1.0) * cs),
                (a + 1.0) + (a - 1.0) * cs - sa,
            )
        }
        BandType::HighShelf => {
            let a = 10.0f64.powf(band.gain_db / 40.0);
            let sa = 2.0 * a.sqrt() * alpha;
            (
                a * ((a + 1.0) + (a - 1.0) * cs + sa),
                -2.0 * a * ((a - 1.0) + (a + 1.0) * cs),
                a * ((a + 1.0) + (a - 1.0) * cs - sa),
                (a + 1.0) - (a - 1.0) * cs + sa,
                2.0 * ((a - 1.0) - (a + 1.0) * cs),
                (a + 1.0) - (a - 1.0) * cs - sa,
            )
        }
    };

    let a0_inv = 1.0 / a0;
    BiquadCoeffs {
        b0: b0 * a0_inv,
        b1: b1 * a0_inv,
        b2: b2 * a0_inv,
        a1: a1 * a0_inv,
        a2: a2 * a0_inv,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn eq_passthrough_no_bands() {
        let config = EqConfig { bands: vec![] };
        let mut eq = EqState::<f32>::new(config, 44100.0);
        let input = [1.0f32, 2.0, 3.0, 4.0];
        let mut output = [0.0f32; 4];
        eq.process_slice(&input, &mut output);
        assert_eq!(output, input);
    }

    #[test]
    fn eq_does_not_panic() {
        let config = EqConfig {
            bands: vec![
                EqBandConfig {
                    freq: 1000.0,
                    q: 1.0,
                    gain_db: 3.0,
                    band_type: BandType::Peak,
                },
                EqBandConfig {
                    freq: 200.0,
                    q: 0.71,
                    gain_db: -2.0,
                    band_type: BandType::LowShelf,
                },
            ],
        };
        let mut eq = EqState::<f32>::new(config, 44100.0);
        let input = [0.5f32; 64];
        let mut output = [0.0f32; 64];
        eq.process_slice(&input, &mut output);
        for v in &output {
            assert!(v.is_finite(), "EQ output should be finite");
        }
    }
}