synth_utils/
glide_processor.rs

1//! # Glide Processor
2//!
3//! The glide processor is used to add variable lag to control signals. Music synthesizers often have a glide control
4//! so that new notes slide into each other instead of stepping directly to the new note.
5//!
6//! The terms glide, lag, and portamento are often used interchangeably.
7
8use crate::utils::*;
9use biquad::*;
10
11/// A glide processor for implementing portamento is represented here.
12pub struct GlideProcessor {
13    // min and max cutoff frequencies
14    min_fc: f32,
15    max_fc: f32,
16
17    // sampe rate in hertz
18    fs: Hertz<f32>,
19
20    // internal lowpass filter to implement the glide
21    lpf: DirectForm1<f32>,
22
23    // cached val to avoid recalculating unnecessarily
24    cached_t: f32,
25}
26
27impl GlideProcessor {
28    /// `GlideProcessor::new(sr)` is a new glide processor with sample rate `sr`
29    pub fn new(sample_rate_hz: f32) -> Self {
30        let max_fc = sample_rate_hz / 2.0_f32;
31
32        let coeffs = coeffs(sample_rate_hz.hz(), max_fc.hz());
33
34        Self {
35            max_fc,
36            min_fc: 0.1_f32,
37            fs: sample_rate_hz.hz(),
38            lpf: DirectForm1::<f32>::new(coeffs),
39            cached_t: -1.0_f32, // initialized such that it always updates the first go-round
40        }
41    }
42
43    /// `gp.set_time(t)` sets the portamento time for the glide processor to the new time `t`
44    ///
45    /// # Arguments:
46    ///
47    /// * `t` - the new value for the glide control time, in `[0.0, 10.0]`
48    ///
49    /// Times that would be faster than sample_rate/2 are clamped.
50    ///
51    /// This function can be somewhat costly, so don't call it more than necessary
52    pub fn set_time(&mut self, t: f32) {
53        // don't update the coefficients if you don't need to, it is costly
54        let epsilon = 0.05_f32;
55        if is_almost(t, self.cached_t, epsilon) {
56            return;
57        }
58
59        self.cached_t = t;
60
61        let f0 = (1.0_f32 / t).max(self.min_fc).min(self.max_fc);
62        self.lpf.update_coefficients(coeffs(self.fs, f0.hz()))
63    }
64
65    /// `gp.process(v)` is the value `v` processed by the glide processor, must be called periodically at the sample rate
66    pub fn process(&mut self, val: f32) -> f32 {
67        self.lpf.run(val)
68    }
69}
70
71/// `coeffs(fs, f0)` is the lowpass filter coefficients for sample rate `fs`, cutoff frequency `f0`, and Q = 0
72fn coeffs(fs: Hertz<f32>, f0: Hertz<f32>) -> Coefficients<f32> {
73    Coefficients::<f32>::from_params(Type::SinglePoleLowPass, fs, f0, 0.0_f32).unwrap()
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn gets_close_to_target_in_t() {
82        let mut glide = GlideProcessor::new(1_000.0);
83        glide.set_time(0.5);
84
85        // start at zero
86        glide.process(0.0);
87
88        // step to 1
89        for _ in 0..499 {
90            glide.process(1.0);
91        }
92
93        // it should get very close to the target
94        assert!(is_almost(glide.process(1.0), 1.0, 0.005));
95    }
96
97    #[test]
98    fn is_monotonic() {
99        let mut glide = GlideProcessor::new(1_000.0);
100        glide.set_time(0.5);
101
102        let mut last_res = glide.process(0.0);
103        for _ in 0..499 {
104            let res = glide.process(1.0);
105            assert!(last_res < res);
106            last_res = res;
107        }
108    }
109}