Skip to main content

rill_patchbay/automaton/
lfo.rs

1//! LFO (Low Frequency Oscillator) automata for periodic modulation.
2//!
3//! Supports various waveform shapes and synchronisation modes.
4
5use crate::engine::{Automaton, NoAction, Range, Time};
6use rill_core::traits::ParamValue;
7use std::f64::consts::PI;
8
9/// LFO waveform shape.
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum LfoWaveform {
13    /// Smooth sinusoidal wave.
14    Sine,
15    /// Triangular wave.
16    Triangle,
17    /// Rising sawtooth wave.
18    Saw,
19    /// Falling sawtooth wave.
20    ReverseSaw,
21    /// Square wave.
22    Square,
23    /// Pulse wave with configurable duty cycle.
24    Pulse(f64),
25    /// Random value held for the duration of one period.
26    SampleAndHold,
27    /// Smooth random walk (continuous noise).
28    RandomWalk,
29}
30
31impl LfoWaveform {
32    /// Return the human-readable name of this waveform.
33    pub fn name(&self) -> &'static str {
34        match self {
35            LfoWaveform::Sine => "Sine",
36            LfoWaveform::Triangle => "Triangle",
37            LfoWaveform::Saw => "Saw",
38            LfoWaveform::ReverseSaw => "Reverse Saw",
39            LfoWaveform::Square => "Square",
40            LfoWaveform::Pulse(_) => "Pulse",
41            LfoWaveform::SampleAndHold => "S&H",
42            LfoWaveform::RandomWalk => "Random Walk",
43        }
44    }
45
46    /// Evaluate the waveform at a given phase (0.0 – 1.0).
47    ///
48    /// `pulse_width` overrides the built-in width for `Pulse` waveforms.
49    pub fn evaluate(&self, phase: f64, pulse_width: Option<f64>) -> f64 {
50        match self {
51            LfoWaveform::Sine => (phase * 2.0 * PI).sin(),
52
53            LfoWaveform::Triangle => {
54                if phase < 0.25 {
55                    4.0 * phase
56                } else if phase < 0.75 {
57                    2.0 - 4.0 * phase
58                } else {
59                    4.0 * phase - 4.0
60                }
61            }
62
63            LfoWaveform::Saw => 2.0 * phase - 1.0,
64
65            LfoWaveform::ReverseSaw => 1.0 - 2.0 * phase,
66
67            LfoWaveform::Square => {
68                if phase < 0.5 {
69                    1.0
70                } else {
71                    -1.0
72                }
73            }
74
75            LfoWaveform::Pulse(width) => {
76                let w = pulse_width.unwrap_or(*width);
77                if phase < w {
78                    1.0
79                } else {
80                    -1.0
81                }
82            }
83
84            LfoWaveform::SampleAndHold => phase,
85
86            LfoWaveform::RandomWalk => phase,
87        }
88    }
89}
90
91/// An LFO automaton that generates periodic modulation signals.
92///
93/// Supports multiple waveform shapes, configurable frequency, amplitude,
94/// offset, pulse width, and random-walk rate.
95#[derive(Debug, Clone)]
96pub struct LfoAutomaton {
97    name: String,
98    frequency: f64,
99    amplitude: f64,
100    offset: f64,
101    waveform: LfoWaveform,
102    range: Range,
103    pulse_width: f64,
104    walk_rate: f64,
105}
106
107impl LfoAutomaton {
108    /// Create a new LFO automaton.
109    pub fn new(
110        name: &str,
111        frequency: f64,
112        amplitude: f64,
113        offset: f64,
114        waveform: LfoWaveform,
115    ) -> Self {
116        Self {
117            name: name.to_string(),
118            frequency: frequency.max(0.001),
119            amplitude,
120            offset,
121            waveform,
122            range: Range::bipolar(),
123            pulse_width: 0.5,
124            walk_rate: 0.1,
125        }
126    }
127
128    /// Set the output range.
129    pub fn with_range(mut self, range: Range) -> Self {
130        self.range = range;
131        self
132    }
133
134    /// Set the pulse width for the `Pulse` waveform (0.01 – 0.99).
135    pub fn with_pulse_width(mut self, width: f64) -> Self {
136        self.pulse_width = width.clamp(0.01, 0.99);
137        self
138    }
139
140    /// Set the random-walk step rate.
141    pub fn with_walk_rate(mut self, rate: f64) -> Self {
142        self.walk_rate = rate.max(0.0);
143        self
144    }
145}
146
147impl Automaton for LfoAutomaton {
148    type Internal = f64;
149    type Action = NoAction;
150
151    fn step(
152        &self,
153        phase: &mut Self::Internal,
154        _current: &ParamValue,
155        time: Time,
156        _action: &Self::Action,
157    ) -> ParamValue {
158        *phase = (time * self.frequency).fract();
159        let raw = match self.waveform {
160            LfoWaveform::Sine => (*phase * 2.0 * PI).sin(),
161            LfoWaveform::Triangle => {
162                if *phase < 0.5 {
163                    4.0 * *phase - 1.0
164                } else {
165                    3.0 - 4.0 * *phase
166                }
167            }
168            LfoWaveform::Saw => 2.0 * *phase - 1.0,
169            LfoWaveform::ReverseSaw => 1.0 - 2.0 * *phase,
170            LfoWaveform::Square => {
171                if *phase < 0.5 {
172                    1.0
173                } else {
174                    -1.0
175                }
176            }
177            LfoWaveform::Pulse(width) => {
178                if *phase < width {
179                    1.0
180                } else {
181                    -1.0
182                }
183            }
184            LfoWaveform::SampleAndHold => {
185                // hold current value, phase changes
186                return ParamValue::Float((*phase * 2.0 * PI).sin() as f32);
187            }
188            LfoWaveform::RandomWalk => {
189                // smooth random — return sine for now
190                (*phase * 2.0 * PI).sin()
191            }
192        };
193        let val = raw * self.amplitude + self.offset;
194        ParamValue::Float(val as f32)
195    }
196
197    fn initial_internal(&self) -> Self::Internal {
198        0.0
199    }
200
201    fn name(&self) -> &str {
202        &self.name
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use float_cmp::approx_eq;
210
211    #[test]
212    fn test_sine_lfo() {
213        let lfo = LfoAutomaton::new("Sine", 1.0, 1.0, 0.0, LfoWaveform::Sine);
214        let mut phase = lfo.initial_internal();
215        let current = ParamValue::Float(0.0);
216
217        let value = lfo.step(&mut phase, &current, 0.0, &NoAction);
218        let val = value.as_f32().unwrap();
219        assert!(approx_eq!(f64, val as f64, 0.0, epsilon = 0.01));
220
221        let value = lfo.step(&mut phase, &current, 0.25, &NoAction);
222        let val = value.as_f32().unwrap();
223        assert!(approx_eq!(f64, val as f64, 1.0, epsilon = 0.01));
224    }
225}