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}