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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
//! # Low Frequency Oscillator
//!
//! ## Acronyms used:
//!
//! - `LFO`: Low Frequency Oscillator
//! - `LUT`: Look Up Table
//! - `DDS`: Direct Digital Synthesis
//!
//! LFOs are a standard component of most analog synthesizers. They are used to
//! modulate various parameters such as loudness, timbre, or pitch.
//!
//! This LFO has a variety of common waveforms available.
//!
//! Since this oscillator is intended as a low frequency control source, no
//! attempts at antialiasing are made. The harmonically rich waveforms (saw, square)
//! will alias even well below nyquist/2. Since there is no reconstruction
//! filter built in even the sine output will alias when the frequency is high.
//!
//! This is not objectionable when the frequency of the LFO is much lower than
//! audio frequencies and it is used to modulate parameters like filter cutoff
//! or provide VCO vibrato, which is the typical use case of this module.
//! Further, the user may wish to create crazy sci-fi effects by intentionally
//! setting the frequency high enough to cause audible aliasing, I don't judge.

use crate::{lookup_tables, phase_accumulator::PhaseAccumulator, utils::*};

/// A Low Frequency Oscillator is represented here
pub struct Lfo {
    phase_accumulator: PhaseAccumulator<TOT_NUM_ACCUM_BITS, NUM_LUT_INDEX_BITS>,
}

impl Lfo {
    /// `Lfo::new(sr)` is a new LFO with sample rate `sr`
    pub fn new(sample_rate_hz: f32) -> Self {
        Self {
            phase_accumulator: PhaseAccumulator::new(sample_rate_hz),
        }
    }

    /// `lfo.tick()` advances the LFO by 1 tick, must be called at the sample rate
    pub fn tick(&mut self) {
        self.phase_accumulator.tick()
    }

    /// `lfo.set_frequency(f)` sets the frequency of the LFO to `f`
    pub fn set_frequency(&mut self, freq: f32) {
        self.phase_accumulator.set_frequency(freq)
    }

    /// `lfo.get(ws)` is the current value of the given waveshape in `[-1.0, +1.0]`
    pub fn get(&self, waveshape: Waveshape) -> f32 {
        match waveshape {
            Waveshape::Sine => {
                let lut_idx = self.phase_accumulator.index();
                let next_lut_idx = (lut_idx + 1) % (lookup_tables::SINE_LUT_SIZE - 1);
                let y0 = lookup_tables::SINE_TABLE[lut_idx];
                let y1 = lookup_tables::SINE_TABLE[next_lut_idx];
                linear_interp(y0, y1, self.phase_accumulator.fraction())
            }
            Waveshape::Triangle => {
                // convert the phase accum ramp into a triangle in-phase with the sine
                let raw_ramp = self.phase_accumulator.ramp() * 4.0;
                if raw_ramp < 1.0_f32 {
                    // starting at zero and ramping up towards positive 1
                    raw_ramp
                } else if raw_ramp < 3.0_f32 {
                    // ramping down through zero towards negative 1
                    2.0_f32 - raw_ramp
                } else {
                    // ramping back up towards zero
                    raw_ramp - 4.0_f32
                }
            }
            Waveshape::UpSaw => (self.phase_accumulator.ramp() * 2.0_f32) - 1.0_f32,
            Waveshape::DownSaw => -self.get(Waveshape::UpSaw),
            Waveshape::Square => {
                if self.phase_accumulator.ramp() < 0.5 {
                    1.0
                } else {
                    -1.0
                }
            }
        }
    }
}

/// LFO waveshapes are represented here
///
/// All waveshapes are simultaneously available
#[derive(Clone, Copy)]
pub enum Waveshape {
    Sine,
    Triangle,
    UpSaw,
    DownSaw,
    Square,
}

/// The total number of bits to use for the phase accumulator
///
/// Must be in `[1..32]`
const TOT_NUM_ACCUM_BITS: u32 = 24;

/// The number of index bits, depends on the lookup tables used
///
/// Note that the lookup table size MUST be a power of 2
const NUM_LUT_INDEX_BITS: u32 = ilog_2(lookup_tables::SINE_LUT_SIZE);

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

    #[test]
    fn sqr_starts_high_and_then_goes_low() {
        let mut lfo = Lfo::new(1_000.0_f32);
        lfo.set_frequency(1.0);

        assert_eq!(lfo.get(Waveshape::Square), 1.0);

        // tick halfway through 1 cycle
        for _ in 0..500 {
            lfo.tick();
        }
        assert_eq!(lfo.get(Waveshape::Square), 1.0);

        // one mode tick makes it flop the low half of the cycle
        lfo.tick();
        assert_eq!(lfo.get(Waveshape::Square), -1.0);
    }

    #[test]
    fn triangle_goes_up_then_down_then_back_up() {
        let epsilon = 0.0001;

        let mut lfo = Lfo::new(1_000.0_f32);
        lfo.set_frequency(1.0);

        assert_eq!(lfo.get(Waveshape::Triangle), 0.0);

        // tick 1/4 through 1 cycle, just hit the positive peak
        for _ in 0..250 {
            lfo.tick();
        }
        assert!(is_almost(lfo.get(Waveshape::Triangle), 1.0, epsilon));

        // tick to the halfway point, back to zero
        for _ in 0..250 {
            lfo.tick();
        }
        assert!(is_almost(lfo.get(Waveshape::Triangle), 0.0, epsilon));

        // another quarter cycle puts us at the lowest point
        for _ in 0..250 {
            lfo.tick();
        }
        assert!(is_almost(lfo.get(Waveshape::Triangle), -1.0, epsilon));
    }

    #[test]
    fn check_a_few_sine_points() {
        let epsilon = 0.001;

        let mut lfo = Lfo::new(10_000.0_f32);
        lfo.set_frequency(1.0);

        // tick 1/10 through 1 cycle
        for _ in 0..1_000 {
            lfo.tick();
        }

        assert!(is_almost(
            lfo.get(Waveshape::Sine),
            f32::sin(core::f32::consts::PI / 5.),
            epsilon
        ));

        // tick to about 45 degrees, but we won't hit it exactly
        for _ in 0..250 {
            lfo.tick();
        }
        assert!(
            (1. / 2.) < lfo.get(Waveshape::Sine) && lfo.get(Waveshape::Sine) < (f32::sqrt(3.) / 2.)
        );

        // tick a bit past 330 degrees
        for _ in 0..7915 {
            lfo.tick();
        }
        assert!((-1. / 2.) < lfo.get(Waveshape::Sine) && lfo.get(Waveshape::Sine) < 0.);
    }

    #[test]
    fn up_saw_is_monotonic_rising() {
        let mut lfo = Lfo::new(100.0_f32);
        lfo.set_frequency(1.0);

        let mut last_val = -1.1;

        for _ in 0..100 {
            lfo.tick();
            assert!(last_val < lfo.get(Waveshape::UpSaw));
            last_val = lfo.get(Waveshape::UpSaw);
        }

        // one more tick rolls it over
        lfo.tick();
        assert!(lfo.get(Waveshape::UpSaw) < last_val);
    }

    #[test]
    fn down_saw_is_just_negated_up_saw() {
        let mut lfo = Lfo::new(100.0_f32);
        lfo.set_frequency(1.0);

        for _ in 0..100 {
            lfo.tick();
            assert_eq!(lfo.get(Waveshape::UpSaw), -lfo.get(Waveshape::DownSaw));
        }
    }
}