Skip to main content

rust_audio_api/nodes/
filter.rs

1use crate::types::AudioUnit;
2use std::f32::consts::PI;
3
4/// Supported biquad filter types.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum FilterType {
7    /// Low-pass filter: Allows frequencies below the cutoff to pass.
8    LowPass,
9    /// High-pass filter: Allows frequencies above the cutoff to pass.
10    HighPass,
11    /// Band-pass filter: Allows frequencies within a range around the cutoff to pass.
12    BandPass,
13}
14
15/// Biquad IIR filter coefficients (Direct Form I)
16#[derive(Debug, Clone, Copy)]
17struct BiquadCoefficients {
18    b0: f32,
19    b1: f32,
20    b2: f32,
21    a1: f32,
22    a2: f32,
23}
24
25/// Per-channel delay state
26#[derive(Debug, Clone, Copy, Default)]
27struct ChannelState {
28    x1: f32, // input z^-1
29    x2: f32, // input z^-2
30    y1: f32, // output z^-1
31    y2: f32, // output z^-2
32}
33
34/// A biquad IIR filter node.
35///
36/// `FilterNode` provides standard LowPass, HighPass, and BandPass filtering.
37/// It supports dynamic updates of cutoff frequency and Q factor via
38/// [`ControlMessage::SetParameter`](crate::graph::ControlMessage::SetParameter).
39///
40/// # Example
41/// ```no_run
42/// use rust_audio_api::nodes::{FilterNode, FilterType, NodeType};
43/// use rust_audio_api::{AudioContext, NodeParameter};
44///
45/// let mut ctx = AudioContext::new().unwrap();
46/// let sample_rate = ctx.sample_rate();
47///
48/// let mut filter_id = None;
49/// let dest_id = ctx.build_graph(|builder| {
50///     let filter = FilterNode::new(FilterType::LowPass, sample_rate, 1000.0, 0.707);
51///     let id = builder.add_node(NodeType::Filter(filter));
52///     filter_id = Some(id);
53///     id
54/// });
55///
56/// // Dynamically sweep the filter cutoff frequency to 2000 Hz
57/// ctx.control_sender().send(
58///     rust_audio_api::graph::ControlMessage::SetParameter(
59///         filter_id.unwrap(),
60///         NodeParameter::Cutoff(2000.0)
61///     )
62/// ).unwrap();
63/// ```
64pub struct FilterNode {
65    filter_type: FilterType,
66    sample_rate: f32,
67    cutoff: f32,
68    q: f32,
69    coeffs: BiquadCoefficients,
70    state: [ChannelState; 2], // L / R
71}
72
73impl FilterNode {
74    /// Creates a new `FilterNode`.
75    ///
76    /// # Parameters
77    /// - `filter_type`: The type of filter ([`FilterType`]).
78    /// - `sample_rate`: Processing sample rate.
79    /// - `cutoff_hz`: Cutoff frequency in Hz.
80    /// - `q`: Quality factor (Resonance).
81    pub fn new(filter_type: FilterType, sample_rate: u32, cutoff_hz: f32, q: f32) -> Self {
82        let mut node = Self {
83            filter_type,
84            sample_rate: sample_rate as f32,
85            cutoff: cutoff_hz,
86            q,
87            coeffs: BiquadCoefficients {
88                b0: 0.0,
89                b1: 0.0,
90                b2: 0.0,
91                a1: 0.0,
92                a2: 0.0,
93            },
94            state: [ChannelState::default(); 2],
95        };
96        node.recalculate_coefficients();
97        node
98    }
99
100    /// Sets the cutoff frequency (updates coefficients automatically).
101    pub fn set_cutoff(&mut self, cutoff_hz: f32) {
102        self.cutoff = cutoff_hz;
103        self.recalculate_coefficients();
104    }
105
106    /// Sets the quality factor Q (updates coefficients automatically).
107    pub fn set_q(&mut self, q: f32) {
108        self.q = q;
109        self.recalculate_coefficients();
110    }
111
112    /// Sets the filter type (updates coefficients automatically).
113    pub fn set_filter_type(&mut self, filter_type: FilterType) {
114        self.filter_type = filter_type;
115        self.recalculate_coefficients();
116    }
117
118    /// Calculates biquad coefficients based on Audio Cookbook (Robert Bristow-Johnson) formulas
119    fn recalculate_coefficients(&mut self) {
120        let w0 = 2.0 * PI * self.cutoff / self.sample_rate;
121        let cos_w0 = w0.cos();
122        let sin_w0 = w0.sin();
123        let alpha = sin_w0 / (2.0 * self.q);
124
125        let (b0, b1, b2, a0, a1, a2) = match self.filter_type {
126            FilterType::LowPass => {
127                let b1 = 1.0 - cos_w0;
128                let b0 = b1 / 2.0;
129                let b2 = b0;
130                let a0 = 1.0 + alpha;
131                let a1 = -2.0 * cos_w0;
132                let a2 = 1.0 - alpha;
133                (b0, b1, b2, a0, a1, a2)
134            }
135            FilterType::HighPass => {
136                let b1_raw = 1.0 + cos_w0;
137                let b0 = b1_raw / 2.0;
138                let b1 = -(1.0 + cos_w0);
139                let b2 = b0;
140                let a0 = 1.0 + alpha;
141                let a1 = -2.0 * cos_w0;
142                let a2 = 1.0 - alpha;
143                (b0, b1, b2, a0, a1, a2)
144            }
145            FilterType::BandPass => {
146                let b0 = alpha;
147                let b1 = 0.0;
148                let b2 = -alpha;
149                let a0 = 1.0 + alpha;
150                let a1 = -2.0 * cos_w0;
151                let a2 = 1.0 - alpha;
152                (b0, b1, b2, a0, a1, a2)
153            }
154        };
155
156        // Normalization: divide all coefficients by a0
157        let inv_a0 = 1.0 / a0;
158        self.coeffs = BiquadCoefficients {
159            b0: b0 * inv_a0,
160            b1: b1 * inv_a0,
161            b2: b2 * inv_a0,
162            a1: a1 * inv_a0,
163            a2: a2 * inv_a0,
164        };
165    }
166
167    /// Performs Direct Form I biquad filtering on a single sample
168    #[inline(always)]
169    fn process_sample(coeffs: &BiquadCoefficients, state: &mut ChannelState, x: f32) -> f32 {
170        let y = coeffs.b0 * x + coeffs.b1 * state.x1 + coeffs.b2 * state.x2
171            - coeffs.a1 * state.y1
172            - coeffs.a2 * state.y2;
173
174        state.x2 = state.x1;
175        state.x1 = x;
176        state.y2 = state.y1;
177        state.y1 = y;
178
179        y
180    }
181
182    #[inline(always)]
183    pub fn process(&mut self, input: Option<&AudioUnit>, output: &mut AudioUnit) {
184        if let Some(in_unit) = input {
185            let coeffs = self.coeffs;
186            output.copy_from_slice(in_unit);
187
188            dasp::slice::map_in_place(&mut output[..], |frame| {
189                let left = Self::process_sample(&coeffs, &mut self.state[0], frame[0]);
190                let right = Self::process_sample(&coeffs, &mut self.state[1], frame[1]);
191                [left, right]
192            });
193        } else {
194            dasp::slice::equilibrium(&mut output[..]);
195        }
196    }
197}