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}