1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//! # Glide Processor
//!
//! The glide processor is used to add variable lag to control signals. Music synthesizers often have a glide control
//! so that new notes slide into each other instead of stepping directly to the new note.
//!
//! The terms glide, lag, and portamento are often used interchangeably.

use crate::utils::*;
use biquad::*;

/// A glide processor for implementing portamento is represented here.
pub struct GlideProcessor {
    // min and max cutoff frequencies
    min_fc: f32,
    max_fc: f32,

    // sampe rate in hertz
    fs: Hertz<f32>,

    // internal lowpass filter to implement the glide
    lpf: DirectForm1<f32>,

    // cached val to avoid recalculating unnecessarily
    cached_t: f32,
}

impl GlideProcessor {
    /// `GlideProcessor::new(sr)` is a new glide processor with sample rate `sr`
    pub fn new(sample_rate_hz: f32) -> Self {
        let max_fc = sample_rate_hz / 2.0_f32;

        let coeffs = coeffs(sample_rate_hz.hz(), max_fc.hz());

        Self {
            max_fc,
            min_fc: 0.1_f32,
            fs: sample_rate_hz.hz(),
            lpf: DirectForm1::<f32>::new(coeffs),
            cached_t: -1.0_f32, // initialized such that it always updates the first go-round
        }
    }

    /// `gp.set_time(t)` sets the portamento time for the glide processor to the new time `t`
    ///
    /// # Arguments:
    ///
    /// * `t` - the new value for the glide control time, in `[0.0, 10.0]`
    ///
    /// Times that would be faster than sample_rate/2 are clamped.
    ///
    /// This function can be somewhat costly, so don't call it more than necessary
    pub fn set_time(&mut self, t: f32) {
        // don't update the coefficients if you don't need to, it is costly
        let epsilon = 0.05_f32;
        if is_almost(t, self.cached_t, epsilon) {
            return;
        }

        self.cached_t = t;

        let f0 = (1.0_f32 / t).max(self.min_fc).min(self.max_fc);
        self.lpf.update_coefficients(coeffs(self.fs, f0.hz()))
    }

    /// `gp.process(v)` is the value `v` processed by the glide processor, must be called periodically at the sample rate
    pub fn process(&mut self, val: f32) -> f32 {
        self.lpf.run(val)
    }
}

/// `coeffs(fs, f0)` is the lowpass filter coefficients for sample rate `fs`, cutoff frequency `f0`, and Q = 0
fn coeffs(fs: Hertz<f32>, f0: Hertz<f32>) -> Coefficients<f32> {
    Coefficients::<f32>::from_params(Type::SinglePoleLowPass, fs, f0, 0.0_f32).unwrap()
}

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

    #[test]
    fn gets_close_to_target_in_t() {
        let mut glide = GlideProcessor::new(1_000.0);
        glide.set_time(0.5);

        // start at zero
        glide.process(0.0);

        // step to 1
        for _ in 0..499 {
            glide.process(1.0);
        }

        // it should get very close to the target
        assert!(is_almost(glide.process(1.0), 1.0, 0.005));
    }

    #[test]
    fn is_monotonic() {
        let mut glide = GlideProcessor::new(1_000.0);
        glide.set_time(0.5);

        let mut last_res = glide.process(0.0);
        for _ in 0..499 {
            let res = glide.process(1.0);
            assert!(last_res < res);
            last_res = res;
        }
    }
}