firewheel-nodes 0.12.1

Official factory nodes for the Firewheel audio engine
Documentation
use super::{all_pass::AllPass, comb::Comb};

const FIXED_GAIN: f64 = 0.015;

const SCALE_WET: f64 = 3.0;
const SCALE_DAMPENING: f64 = 0.4;

const SCALE_ROOM: f64 = 0.28;
const OFFSET_ROOM: f64 = 0.7;

const STEREO_SPREAD: usize = 23;

const COMB_TUNING: [usize; 8] = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
const ALLPASS_TUNING: [usize; 4] = [556, 441, 341, 225];

#[derive(Debug)]
pub struct Freeverb {
    combs: [(Comb, Comb); 8],
    allpasses: [(AllPass, AllPass); 4],
    wet_gains: (f64, f64),
    wet: f64,
    width: f64,
    dry: f64,
    input_gain: f64,
    dampening: f64,
    room_size: f64,
    frozen: bool,
}

fn adjust_length(length: usize, sr: usize) -> usize {
    (length as f64 * sr as f64 / 44100.0) as usize
}

impl Freeverb {
    pub fn new(sample_rate: usize) -> Self {
        let mut freeverb = Freeverb {
            combs: core::array::from_fn(|i| {
                (
                    Comb::new(adjust_length(COMB_TUNING[i], sample_rate)),
                    Comb::new(adjust_length(COMB_TUNING[i] + STEREO_SPREAD, sample_rate)),
                )
            }),
            allpasses: core::array::from_fn(|i| {
                (
                    AllPass::new(adjust_length(ALLPASS_TUNING[i], sample_rate)),
                    AllPass::new(adjust_length(
                        ALLPASS_TUNING[i] + STEREO_SPREAD,
                        sample_rate,
                    )),
                )
            }),
            wet_gains: (0.0, 0.0),
            wet: 0.0,
            dry: 0.0,
            input_gain: 0.0,
            width: 0.0,
            dampening: 0.0,
            room_size: 0.0,
            frozen: false,
        };

        freeverb.set_wet(1.0);
        freeverb.set_width(0.5);
        freeverb.set_dampening(0.5);
        freeverb.set_room_size(0.5);
        freeverb.set_frozen(false);

        freeverb
    }

    pub fn tick(&mut self, input: (f64, f64)) -> (f64, f64) {
        let input_mixed = (input.0 + input.1) * FIXED_GAIN * self.input_gain;

        let mut out = (0.0, 0.0);

        for combs in self.combs.iter_mut() {
            out.0 += combs.0.tick(input_mixed);
            out.1 += combs.1.tick(input_mixed);
        }

        for allpasses in self.allpasses.iter_mut() {
            out.0 = allpasses.0.tick(out.0);
            out.1 = allpasses.1.tick(out.1);
        }

        (
            out.0 * self.wet_gains.0 + out.1 * self.wet_gains.1 + input.0 * self.dry,
            out.1 * self.wet_gains.0 + out.0 * self.wet_gains.1 + input.1 * self.dry,
        )
    }

    pub fn set_dampening(&mut self, value: f64) {
        self.dampening = value * SCALE_DAMPENING;
    }

    pub fn set_wet(&mut self, value: f64) {
        self.wet = value * SCALE_WET;
        self.update_wet_gains();
    }

    pub fn set_width(&mut self, value: f64) {
        self.width = value;
        self.update_wet_gains();
    }

    fn update_wet_gains(&mut self) {
        self.wet_gains = (
            self.wet * (self.width / 2.0 + 0.5),
            self.wet * ((1.0 - self.width) / 2.0),
        )
    }

    fn set_frozen(&mut self, frozen: bool) {
        self.frozen = frozen;
        self.input_gain = if frozen { 0.0 } else { 1.0 };
        self.update_combs();
    }

    pub fn set_room_size(&mut self, value: f64) {
        self.room_size = value * SCALE_ROOM + OFFSET_ROOM;
    }

    pub fn update_combs(&mut self) {
        let (feedback, dampening) = if self.frozen {
            (1.0, 0.0)
        } else {
            (self.room_size, self.dampening)
        };

        for combs in self.combs.iter_mut() {
            combs.0.set_feedback(feedback);
            combs.1.set_feedback(feedback);

            combs.0.set_dampening(dampening);
            combs.1.set_dampening(dampening);
        }
    }

    pub fn reset(&mut self) {
        for (l, r) in self.combs.iter_mut() {
            l.reset();
            r.reset();
        }

        for (l, r) in self.allpasses.iter_mut() {
            l.reset();
            r.reset();
        }
    }

    pub fn resize(&mut self, sample_rate: usize) {
        for (i, (l, r)) in self.combs.iter_mut().enumerate() {
            l.resize(adjust_length(COMB_TUNING[i], sample_rate));
            r.resize(adjust_length(COMB_TUNING[i] + STEREO_SPREAD, sample_rate));
        }

        for (i, (l, r)) in self.allpasses.iter_mut().enumerate() {
            l.resize(adjust_length(ALLPASS_TUNING[i], sample_rate));
            r.resize(adjust_length(
                ALLPASS_TUNING[i] + STEREO_SPREAD,
                sample_rate,
            ));
        }
    }
}

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

    #[test]
    fn ticking_does_something() {
        let mut freeverb = super::Freeverb::new(44100);
        assert_eq!(freeverb.tick((1.0, 1.0)), (0.0, 0.0));
        for _ in 0..(super::COMB_TUNING[7] + STEREO_SPREAD) * 2 {
            freeverb.tick((0.0, 0.0));
        }
        assert_ne!(freeverb.tick((0.0, 0.0)), (0.0, 0.0));
    }
}