Skip to main content

synthrs/
filter.rs

1//! A collection of signal filters.
2//!
3//! To filter a bunch of samples, first create the filter and samples.
4//!
5//! There are two types of filters: stateless, and stateful filters.
6//! Stateless filters can be used to convolve samples, while stateful filters transform individual samples.
7//!
8//! ### Stateless filters
9//!
10//! Stateless filters are pure functions and are used in conjunction with the convolve function:
11//! ```
12//! use synthrs::filter::{convolve, cutoff_from_frequency, lowpass_filter};
13//! use synthrs::synthesizer::{quantize_samples, make_samples};
14//! use synthrs::wave::sine_wave;
15//!
16//! // Generate a bunch of samples at two different frequencies
17//! let samples = make_samples(0.5, 44_100, |t: f64| -> f64 {
18//!     0.5 * (sine_wave(6000.0)(t) + sine_wave(80.0)(t))
19//! });
20//!
21//! // Create a lowpass filter, using a cutoff of 400Hz at a 44_100Hz sample rate (ie. filter out frequencies >400Hz)
22//! let lowpass = lowpass_filter(cutoff_from_frequency(400.0, 44_100), 0.01);
23//!
24//! // Apply convolution to filter out high frequencies
25//! let lowpass_samples = quantize_samples::<i16>(&convolve(&lowpass, &samples));
26//! ```
27//! #### Common stateless filter arguments:
28//!
29//! * `cutoff`: as a fraction of sample rate, can be obtained from
30//!             `cutoff_from_frequency(cutoff, sample_rate)`. (eg. for a lowpass filter
31//!             frequencies below `sample_rate` / `cutoff` are preserved)
32//! * `band`: transition band as a fraction of the sample rate. This determines how
33//!         the cutoff "blends", or how harsh a cutoff this is.
34//!
35//! ### Stateful filters
36//!
37//! Stateful filters are structs which hold some state, such as `DelayLine` which has to
38//! keep in memory historical samples.
39//!
40//! They can be used to transform a bunch of samples using `map`.
41//!
42//! ```
43//! use synthrs::filter::Comb;
44//! use synthrs::synthesizer::{quantize_samples, make_samples};
45//! use synthrs::wave::sine_wave;
46//!
47//! // Creates a comb filter with
48//! // * 0.2 second delay
49//! // * 44100Hz,
50//! // * 0.5 dampening inverse factor
51//! // * 0.5 dampening factor
52//! // * 0.5 feedback factor
53//! let mut comb = Comb::new(0.2, 44_100, 0.5, 0.5, 0.5);
54//!
55//! let samples = make_samples(0.5, 44_100, |t: f64| -> f64 { sine_wave(440.0)(t) });
56//!
57//! let filtered_raw: Vec<f64> = samples
58//!     .into_iter()
59//!     .map(|sample| comb.tick(sample))
60//!     .collect();
61//! let filtered_quantized = quantize_samples::<i16>(&filtered_raw);
62//! ```
63//!
64//! See: `examples/filters.rs`
65//!
66//! An all-poss filter is implemented as a generator in `crate::wave::allpass`.
67
68use std::f64::consts::PI;
69
70/// Creates a low-pass filter. Frequencies below the cutoff are preserved when
71/// samples are convolved with this filter.
72pub fn lowpass_filter(cutoff: f64, band: f64) -> Vec<f64> {
73    let mut n = (4.0 / band).ceil() as usize;
74    if n % 2 == 1 {
75        n += 1;
76    }
77
78    let sinc = |x: f64| -> f64 { (x * PI).sin() / (x * PI) };
79
80    let sinc_wave: Vec<f64> = (0..n)
81        .map(|i| sinc(2.0 * cutoff * (i as f64 - (n as f64 - 1.0) / 2.0)))
82        .collect();
83
84    let blackman_window = blackman_window(n);
85
86    let filter: Vec<f64> = sinc_wave
87        .iter()
88        .zip(blackman_window.iter())
89        .map(|tup| *tup.0 * *tup.1)
90        .collect();
91
92    // Normalize
93    let sum = filter.iter().fold(0.0, |acc, &el| acc + el);
94
95    filter.iter().map(|&el| el / sum).collect()
96}
97
98/// Creates a Blackman window filter of a given size.
99pub fn blackman_window(size: usize) -> Vec<f64> {
100    (0..size)
101        .map(|i| {
102            0.42 - 0.5 * (2.0 * PI * i as f64 / (size as f64 - 1.0)).cos()
103                + 0.08 * (4.0 * PI * i as f64 / (size as f64 - 1.0)).cos()
104        })
105        .collect()
106}
107
108/// Creates a high-pass filter. Frequencies above the cutoff are preserved when
109/// samples are convolved with this filter.
110pub fn highpass_filter(cutoff: f64, band: f64) -> Vec<f64> {
111    spectral_invert(&lowpass_filter(cutoff, band))
112}
113
114/// Creates a low-pass filter. Frequencies between `low_frequency` and `high_frequency`
115/// are preserved when samples are convolved with this filter.
116pub fn bandpass_filter(low_frequency: f64, high_frequency: f64, band: f64) -> Vec<f64> {
117    assert!(low_frequency <= high_frequency);
118    let lowpass = lowpass_filter(high_frequency, band);
119    let highpass = highpass_filter(low_frequency, band);
120    convolve(&highpass, &lowpass)
121}
122
123/// Creates a low-pass filter. Frequencies outside of `low_frequency` and `high_frequency`
124/// are preserved when samples are convolved with this filter.
125pub fn bandreject_filter(low_frequency: f64, high_frequency: f64, band: f64) -> Vec<f64> {
126    assert!(low_frequency <= high_frequency);
127    let lowpass = lowpass_filter(low_frequency, band);
128    let highpass = highpass_filter(high_frequency, band);
129    add(&highpass, &lowpass)
130}
131
132/// Given a filter, inverts it. For example, inverting a low-pass filter will result in a
133/// high-pass filter with the same cutoff frequency.
134pub fn spectral_invert(filter: &[f64]) -> Vec<f64> {
135    assert_eq!(filter.len() % 2, 0);
136    let mut count = 0;
137
138    filter
139        .iter()
140        .map(|&el| {
141            let add = if count == filter.len() / 2 { 1.0 } else { 0.0 };
142            count += 1;
143            -el + add
144        })
145        .collect()
146}
147
148pub fn convolve(filter: &[f64], input: &[f64]) -> Vec<f64> {
149    let mut output: Vec<f64> = Vec::new();
150    let h_len = (filter.len() / 2) as isize;
151
152    for i in -(filter.len() as isize / 2)..(input.len() as isize - 1) {
153        output.push(0.0);
154        for j in 0isize..filter.len() as isize {
155            let input_idx = i + j;
156            let output_idx = i + h_len;
157            if input_idx < 0 || input_idx >= input.len() as isize {
158                continue;
159            }
160            output[output_idx as usize] += input[input_idx as usize] * filter[j as usize]
161        }
162    }
163
164    output
165}
166
167/// Performs elementwise addition of two `Vec<f64>`s. Can be used to combine filters together
168/// (eg. combining a low-pass filter with a high-pass filter to create a band-pass filter)
169pub fn add(left: &[f64], right: &[f64]) -> Vec<f64> {
170    left.iter()
171        .zip(right.iter())
172        .map(|tup| *tup.0 + *tup.1)
173        .collect()
174}
175
176/// Returns the cutoff fraction for a given cutoff frequency at a sample rate, which can be
177/// used for filter creation.
178pub fn cutoff_from_frequency(frequency: f64, sample_rate: usize) -> f64 {
179    frequency / sample_rate as f64
180}
181
182/// Simple linear attack/decay envelope. No sustain or release.
183pub fn envelope(relative_t: f64, attack: f64, decay: f64) -> f64 {
184    if relative_t < 0.0 {
185        return 0.0;
186    } else if relative_t < attack {
187        return relative_t / attack;
188    } else if relative_t < attack + decay {
189        return 1.0 - (relative_t - attack) / decay;
190    }
191
192    0.0
193}
194
195/// A stateful delay line. Samples are delayed for `delay_length` seconds.
196///
197/// https://en.wikipedia.org/wiki/Analog_delay_line
198///
199/// ```
200/// use synthrs::filter::AllPass;
201///
202/// let mut allpass = AllPass::new(1.0, 44_100, 0.5);
203/// let samples: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0];
204///
205/// let filtered = samples.into_iter().map(|sample| allpass.tick(sample));
206/// ```
207///
208/// Taken from: https://github.com/irh/freeverb-rs/blob/master/freeverb/src/delay_line.rs
209#[derive(Clone, Debug)]
210pub struct DelayLine {
211    pub buf: Vec<f64>,
212    index: usize,
213    pub delay_length: f64,
214    pub delay_samples: usize,
215    pub sample_rate: usize,
216}
217
218impl DelayLine {
219    /// Creates a new delay line. Samples are delayed for `delay_length` seconds.
220    pub fn new(delay_length: f64, sample_rate: usize) -> DelayLine {
221        let delay_samples = ((delay_length * sample_rate as f64).round() + 1.0) as usize;
222
223        DelayLine {
224            buf: vec![0.0; delay_samples],
225            index: 0,
226            delay_length,
227            delay_samples,
228            sample_rate,
229        }
230    }
231
232    pub fn read(&self) -> f64 {
233        self.buf[self.index]
234    }
235
236    pub fn write(&mut self, value: f64) {
237        self.buf[self.index] = value;
238
239        if self.index == self.buf.len() - 1 {
240            self.index = 0;
241        } else {
242            self.index += 1;
243        }
244    }
245}
246
247/// A stateful all-pass filter.
248///
249/// https://en.wikipedia.org/wiki/All-pass_filter
250///
251/// ```
252/// use synthrs::filter::AllPass;
253///
254/// let mut allpass = AllPass::new(1.0, 44_100, 0.5);
255/// let samples: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0];
256///
257/// let filtered = samples.into_iter().map(|sample| allpass.tick(sample));
258/// ```
259///
260/// Taken from: https://github.com/irh/freeverb-rs/blob/master/freeverb/src/all_pass.rs
261#[derive(Clone, Debug)]
262pub struct AllPass {
263    delay_line: DelayLine,
264    /// Feedback multiplier (0.5 works)
265    pub feedback: f64,
266}
267
268impl AllPass {
269    /// Creates a new all-pass filter. Samples are delayed for `delay_length` seconds.
270    pub fn new(delay_length: f64, sample_rate: usize, feedback: f64) -> AllPass {
271        AllPass {
272            delay_line: DelayLine::new(delay_length, sample_rate),
273            feedback,
274        }
275    }
276
277    pub fn tick(&mut self, input: f64) -> f64 {
278        let delayed = self.delay_line.read();
279        self.delay_line.write(input + delayed * self.feedback);
280        -input + delayed
281    }
282}
283
284/// A stateful comb filter.
285///
286/// https://en.wikipedia.org/wiki/Comb_filter
287///
288/// ```
289/// use synthrs::filter::Comb;
290///
291/// let mut comb = Comb::new(1.0, 44_100, 0.5, 0.5, 0.5);
292/// let samples: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0];
293///
294/// let filtered = samples.into_iter().map(|sample| comb.tick(sample));
295/// ```
296///
297/// Taken from: https://github.com/irh/freeverb-rs/blob/master/freeverb/src/comb.rs
298#[derive(Clone, Debug)]
299pub struct Comb {
300    delay_line: DelayLine,
301    filter_state: f64,
302    /// 0.5 works
303    pub dampening_inverse: f64,
304    /// 0.5 works
305    pub dampening: f64,
306    /// 0.5 works
307    pub feedback: f64,
308}
309
310impl Comb {
311    /// Creates a new comb filter. Samples are delayed for `delay_length` seconds.
312    pub fn new(
313        delay_length: f64,
314        sample_rate: usize,
315        dampening_inverse: f64,
316        dampening: f64,
317        feedback: f64,
318    ) -> Comb {
319        Comb {
320            dampening_inverse,
321            dampening,
322            delay_line: DelayLine::new(delay_length, sample_rate),
323            feedback,
324            filter_state: 0.0,
325        }
326    }
327
328    pub fn tick(&mut self, input: f64) -> f64 {
329        let output = self.delay_line.read();
330        self.filter_state = output * self.dampening_inverse + self.filter_state * self.dampening;
331        self.delay_line
332            .write(input + self.filter_state * self.feedback);
333
334        output
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn test_convolve() {
344        let filter = vec![1.0, 1.0, 1.0];
345        let input = vec![0.0, 0.0, 3.0, 0.0, 3.0, 0.0, 0.0];
346        let output = vec![0.0, 3.0, 3.0, 6.0, 3.0, 3.0, 0.0];
347        assert_eq!(convolve(&filter, &input), output);
348    }
349
350    #[test]
351    fn test_add() {
352        let a = vec![1.0, -1.0, -8.0];
353        let b = vec![-1.0, 5.0, 3.0];
354        let expected = vec![0.0, 4.0, -5.0];
355        assert_eq!(add(&a, &b), expected);
356    }
357
358    #[test]
359    #[allow(clippy::float_cmp)]
360    fn test_envelope() {
361        assert_eq!(envelope(0.25, 1.0, 1.0), 0.25);
362        assert_eq!(envelope(0.5, 1.0, 1.0), 0.5);
363        assert_eq!(envelope(1.0, 1.0, 1.0), 1.0);
364        assert_eq!(envelope(1.5, 1.0, 1.0), 0.5);
365        assert_eq!(envelope(3.0, 1.0, 1.0), 0.0);
366        assert_eq!(envelope(-0.5, 1.0, 1.0), 0.0);
367    }
368
369    #[test]
370    #[allow(clippy::float_cmp)]
371    fn test_delay_line() {
372        let mut delay_line = DelayLine::new(3.0, 1);
373
374        delay_line.write(1.0);
375        assert_eq!(delay_line.read(), 0.0);
376        delay_line.write(3.0);
377        assert_eq!(delay_line.read(), 0.0);
378        delay_line.write(5.0);
379        assert_eq!(delay_line.read(), 0.0);
380        delay_line.write(7.0);
381        assert_eq!(delay_line.read(), 1.0);
382        delay_line.write(11.0);
383        assert_eq!(delay_line.read(), 3.0);
384        delay_line.write(13.0);
385        assert_eq!(delay_line.read(), 5.0);
386        delay_line.write(17.0);
387        assert_eq!(delay_line.read(), 7.0);
388    }
389}