Skip to main content

rill_core_dsp/generators/
lfo.rs

1//! Low-frequency oscillators for modulation
2//!
3//! LFOs are used for modulating sound parameters:
4//! vibrato (frequency), tremolo (amplitude), filter sweep (cutoff),
5//! and other effects.
6
7use super::basic::{BasicOscillator, Waveform};
8use crate::generators::{Generator, SyncableGenerator};
9use crate::vector::prelude::*;
10use rill_core::traits::algorithm::{Algorithm, AlgorithmCategory, AlgorithmMetadata};
11use rill_core::traits::ProcessResult;
12use rill_core::Transcendental;
13
14/// LFO (Low Frequency Oscillator)
15///
16/// Generates low-frequency signals for parameter modulation.
17/// Frequency range: 0.01 Hz - 100 Hz.
18///
19/// # Operating modes
20///
21/// - **Bipolar**: output in range [-1, 1]
22/// - **Unipolar**: output in range [0, 1]
23///
24/// # Example
25/// ```
26/// use rill_core::time::ClockTick;
27/// use rill_core::traits::ActionContext;
28/// use rill_core_dsp::generators::*;
29/// use rill_core::traits::algorithm::Algorithm;
30///
31/// let tick = ClockTick::default();
32/// let ctx = ActionContext::new(&tick);
33///
34/// // Create LFO for filter frequency modulation
35/// let mut lfo = LFO::<f32>::new(
36///     5.0,              // 5 Hz
37///     Waveform::Sine,
38///     true              // bipolar mode (-1..1)
39/// );
40/// lfo.init(44100.0);
41///
42/// // Generate modulation signal
43/// let mut output = [0.0_f32];
44/// lfo.process(None, &mut output).unwrap();
45/// let modulation = output[0];
46/// ```
47#[derive(Clone, Copy)]
48pub struct LFO<T: Transcendental> {
49    /// Internal oscillator
50    osc: BasicOscillator<T>,
51    /// Bipolar mode (-1..1) or unipolar (0..1)
52    bipolar: bool,
53    /// Phase offset (for synchronization)
54    phase_offset: ScalarVector1<T>,
55}
56
57impl<T: Transcendental> LFO<T> {
58    /// Create a new LFO
59    ///
60    /// # Arguments
61    /// * `frequency` - frequency in Hz (0.01 - 100)
62    /// * `waveform` - waveform shape
63    /// * `bipolar` - true for bipolar (-1..1), false for unipolar (0..1)
64    pub fn new(frequency: f32, waveform: Waveform, bipolar: bool) -> Self {
65        let one = T::from_f32(1.0);
66        Self {
67            osc: BasicOscillator::new(waveform, frequency, one),
68            bipolar,
69            phase_offset: ScalarVector1::splat(T::ZERO),
70        }
71    }
72
73    /// Create an LFO with phase offset
74    pub fn with_phase_offset(mut self, offset: T) -> Self {
75        self.set_phase_offset(offset);
76        self
77    }
78
79    /// Set bipolar mode
80    ///
81    /// # Arguments
82    /// * `bipolar` - true: output in [-1, 1], false: output in [0, 1]
83    pub fn set_bipolar(&mut self, bipolar: bool) {
84        self.bipolar = bipolar;
85    }
86
87    /// Set phase offset (0..1)
88    ///
89    /// Shifts the LFO phase relative to the reference point.
90    /// Useful for stereo effects or synchronizing multiple LFOs.
91    pub fn set_phase_offset(&mut self, offset: T) {
92        let one = T::from_f32(1.0);
93        let zero = T::ZERO;
94        let clamped = if offset > one {
95            one
96        } else if offset < zero {
97            zero
98        } else {
99            offset
100        };
101        self.phase_offset = ScalarVector1::splat(clamped);
102    }
103
104    /// Get current phase offset
105    pub fn phase_offset(&self) -> T {
106        self.phase_offset.extract(0)
107    }
108
109    /// Check if LFO is in bipolar mode
110    pub fn is_bipolar(&self) -> bool {
111        self.bipolar
112    }
113
114    /// Sync with external clock
115    ///
116    /// # Arguments
117    /// * `reset` - if true, reset phase to phase_offset value
118    pub fn sync(&mut self, reset: bool) {
119        if reset {
120            self.osc.set_phase(self.phase_offset.extract(0));
121        }
122    }
123
124    /// Get modulation value (current sample)
125    pub fn modulate(&mut self) -> T {
126        let raw = self.osc.generate().extract(0);
127
128        if self.bipolar {
129            raw // already -1..1
130        } else {
131            // Convert from -1..1 to 0..1
132            raw.mul(T::from_f32(0.5)).add(T::from_f32(0.5))
133        }
134    }
135
136    /// Reset LFO to initial state
137    pub fn reset(&mut self) {
138        self.osc.reset();
139        self.osc.set_phase(self.phase_offset.extract(0));
140    }
141}
142
143// ==================== Algorithm trait implementation ====================
144
145impl<T: Transcendental> Algorithm<T> for LFO<T> {
146    fn init(&mut self, sample_rate: f32) {
147        self.osc.init(sample_rate);
148        self.osc.set_phase(self.phase_offset.extract(0));
149    }
150
151    fn reset(&mut self) {
152        self.osc.reset();
153        self.osc.set_phase(self.phase_offset.extract(0));
154    }
155
156    fn process(&mut self, _input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
157        for out in output.iter_mut() {
158            *out = self.modulate();
159        }
160        Ok(())
161    }
162
163    fn metadata(&self) -> AlgorithmMetadata {
164        // Get waveform name from LFO's internal waveform
165        // No direct access to waveform, so use description from BasicOscillator
166        AlgorithmMetadata {
167            name: "LFO",
168            category: AlgorithmCategory::Generator,
169            description: format!(
170                "{} wave LFO ({}polar)",
171                match self.osc.frequency() {
172                    _ if self.osc.frequency() < 1.0 => "Very low frequency",
173                    _ if self.osc.frequency() < 10.0 => "Low frequency",
174                    _ => "Signal rate",
175                },
176                if self.bipolar { "bi" } else { "uni" }
177            )
178            .leak(),
179            author: "Rill",
180            version: env!("CARGO_PKG_VERSION"),
181        }
182    }
183}
184
185// ==================== Generator trait implementation ====================
186
187impl<T: Transcendental> Generator<T> for LFO<T> {
188    fn phase(&self) -> T {
189        self.osc.phase()
190    }
191
192    fn set_phase(&mut self, phase: T) {
193        self.osc.set_phase(phase);
194    }
195
196    fn frequency(&self) -> f32 {
197        self.osc.frequency()
198    }
199
200    fn set_frequency(&mut self, freq: f32) {
201        self.osc.set_frequency(freq);
202    }
203
204    fn amplitude(&self) -> T {
205        self.osc.amplitude()
206    }
207
208    fn set_amplitude(&mut self, amp: T) {
209        self.osc.set_amplitude(amp);
210    }
211}
212
213// ==================== SyncableGenerator trait implementation ====================
214
215impl<T: Transcendental> SyncableGenerator<T> for LFO<T> {
216    fn sync(&mut self, reset: bool) {
217        if reset {
218            self.osc.set_phase(self.phase_offset.extract(0));
219        }
220    }
221
222    fn periods(&self) -> u32 {
223        self.osc.periods()
224    }
225}
226
227// ==================== Tests ====================
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use float_cmp::approx_eq;
233
234    #[test]
235    fn test_lfo_creation() {
236        let lfo = LFO::<f32>::new(5.0, Waveform::Sine, true);
237        assert_eq!(lfo.frequency(), 5.0);
238        assert!(lfo.is_bipolar());
239        assert_eq!(lfo.phase_offset(), 0.0);
240    }
241
242    #[test]
243    fn test_lfo_bipolar_mode() {
244        let mut lfo = LFO::<f32>::new(5.0, Waveform::Sine, true);
245        lfo.init(44100.0);
246
247        // In bipolar mode, values should be in [-1, 1]
248        let mut output = [0.0f32; 1];
249        for _ in 0..100 {
250            lfo.process(None, &mut output).unwrap();
251            let val = output[0];
252            assert!(
253                (-1.0..=1.0).contains(&val),
254                "Value {} out of range [-1,1]",
255                val
256            );
257        }
258    }
259
260    #[test]
261    fn test_lfo_unipolar_mode() {
262        let mut lfo = LFO::<f32>::new(5.0, Waveform::Sine, false);
263        lfo.init(44100.0);
264
265        // In unipolar mode, values should be in [0, 1]
266        let mut output = [0.0f32; 1];
267        for _ in 0..100 {
268            lfo.process(None, &mut output).unwrap();
269            let val = output[0];
270            assert!(
271                (0.0..=1.0).contains(&val),
272                "Value {} out of range [0,1]",
273                val
274            );
275        }
276    }
277
278    #[test]
279    fn test_lfo_phase_offset() {
280        let mut lfo = LFO::<f32>::new(5.0, Waveform::Sine, true);
281        lfo.set_phase_offset(0.25);
282        lfo.init(44100.0);
283
284        // Verify phase is set correctly
285        assert!(approx_eq!(f32, lfo.phase(), 0.25, epsilon = 0.01));
286    }
287
288    #[test]
289    fn test_lfo_sync() {
290        let mut lfo = LFO::<f32>::new(5.0, Waveform::Sine, true);
291        lfo.set_phase_offset(0.5);
292        lfo.init(44100.0);
293
294        // Advance phase
295        let mut output = [0.0f32; 1];
296        for _ in 0..10 {
297            lfo.process(None, &mut output).unwrap();
298        }
299
300        // Sync with reset
301        lfo.sync(true);
302        assert!(approx_eq!(f32, lfo.phase(), 0.5, epsilon = 0.01));
303    }
304
305    #[test]
306    fn test_lfo_waveforms() {
307        let waveforms = [
308            Waveform::Sine,
309            Waveform::Saw,
310            Waveform::Square,
311            Waveform::Triangle,
312        ];
313
314        for &wav in &waveforms {
315            let mut lfo = LFO::<f32>::new(5.0, wav, true);
316            lfo.init(44100.0);
317
318            let mut output = [0.0f32; 1];
319            lfo.process(None, &mut output).unwrap();
320            let val = output[0];
321            assert!(
322                (-1.0..=1.0).contains(&val),
323                "Waveform {:?} produced {}",
324                wav,
325                val
326            );
327        }
328    }
329
330    #[test]
331    fn test_lfo_generator_trait() {
332        let mut lfo = LFO::<f32>::new(5.0, Waveform::Sine, true);
333        lfo.init(44100.0);
334
335        // Test methods from Generator trait
336        assert_eq!(lfo.frequency(), 5.0);
337
338        lfo.set_frequency(10.0);
339        assert_eq!(lfo.frequency(), 10.0);
340
341        lfo.set_amplitude(0.5);
342        assert_eq!(lfo.amplitude(), 0.5);
343
344        let phase = lfo.phase();
345        assert!((0.0..=1.0).contains(&phase));
346    }
347
348    #[test]
349    fn test_lfo_syncable_trait() {
350        let mut lfo = LFO::<f32>::new(5.0, Waveform::Sine, true);
351        lfo.init(44100.0);
352
353        let initial_periods = lfo.periods();
354        println!("Initial periods: {}", initial_periods);
355
356        // Compute samples per period
357        let samples_per_period = (44100.0 / 5.0) as usize; // 8820 samples
358        println!("Samples per period: {}", samples_per_period);
359
360        // Record initial phase
361        let initial_phase = lfo.phase();
362        println!("Initial phase: {}", initial_phase.to_f32());
363        let mut output = [0.0f32; 1];
364
365        // Advance phase through several periods
366        for i in 0..samples_per_period * 3 {
367            // 3 full periods
368            let before_phase = lfo.phase();
369            lfo.process(None, &mut output).unwrap();
370            let after_phase = lfo.phase();
371
372            // Check if phase reset occurred
373            if after_phase < before_phase {
374                println!(
375                    "Phase reset at sample {}: {} -> {}",
376                    i,
377                    before_phase.to_f32(),
378                    after_phase.to_f32()
379                );
380                println!("Periods count: {}", lfo.periods());
381            }
382
383            // Debug output at key points
384            if i == samples_per_period - 1 {
385                println!(
386                    "After 1 period (sample {}): phase={}, periods={}",
387                    i,
388                    lfo.phase().to_f32(),
389                    lfo.periods()
390                );
391            } else if i == samples_per_period * 2 - 1 {
392                println!(
393                    "After 2 periods (sample {}): phase={}, periods={}",
394                    i,
395                    lfo.phase().to_f32(),
396                    lfo.periods()
397                );
398            }
399        }
400
401        println!("Final phase: {}", lfo.phase().to_f32());
402        println!("Final periods: {}", lfo.periods());
403
404        assert!(
405            lfo.periods() > initial_periods,
406            "Periods should increase: before={}, after={}",
407            initial_periods,
408            lfo.periods()
409        );
410
411        // Verify phase continues to change
412        let mid_phase = lfo.phase();
413        assert!(mid_phase != initial_phase, "Phase should change");
414
415        // Sync with reset
416        lfo.sync(true);
417        assert!(approx_eq!(f32, lfo.phase(), 0.0, epsilon = 0.01));
418    }
419
420    #[test]
421    fn test_lfo_clone_copy() {
422        let lfo1 = LFO::<f32>::new(5.0, Waveform::Sine, true);
423        let lfo2 = lfo1; // Copy
424        let lfo3 = Clone::clone(&lfo1); // Explicit clone
425
426        assert_eq!(lfo1.frequency(), lfo2.frequency());
427        assert_eq!(lfo1.frequency(), lfo3.frequency());
428        assert_eq!(lfo1.is_bipolar(), lfo2.is_bipolar());
429    }
430}