1use crate::control::{Automaton, Range, Time};
7use std::f64::consts::PI;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum LfoWaveform {
12 Sine,
14 Triangle,
16 Saw,
18 ReverseSaw,
20 Square,
22 Pulse(f64), SampleAndHold,
26 RandomWalk,
28}
29
30impl LfoWaveform {
31 pub fn name(&self) -> &'static str {
33 match self {
34 LfoWaveform::Sine => "Sine",
35 LfoWaveform::Triangle => "Triangle",
36 LfoWaveform::Saw => "Saw",
37 LfoWaveform::ReverseSaw => "Reverse Saw",
38 LfoWaveform::Square => "Square",
39 LfoWaveform::Pulse(_) => "Pulse",
40 LfoWaveform::SampleAndHold => "S&H",
41 LfoWaveform::RandomWalk => "Random Walk",
42 }
43 }
44
45 pub fn evaluate(&self, phase: f64, pulse_width: Option<f64>) -> f64 {
47 match self {
48 LfoWaveform::Sine => (phase * 2.0 * PI).sin(),
49
50 LfoWaveform::Triangle => {
51 if phase < 0.25 {
52 4.0 * phase
53 } else if phase < 0.75 {
54 2.0 - 4.0 * phase
55 } else {
56 4.0 * phase - 4.0
57 }
58 }
59
60 LfoWaveform::Saw => 2.0 * phase - 1.0,
61
62 LfoWaveform::ReverseSaw => 1.0 - 2.0 * phase,
63
64 LfoWaveform::Square => {
65 if phase < 0.5 {
66 1.0
67 } else {
68 -1.0
69 }
70 }
71
72 LfoWaveform::Pulse(width) => {
73 let w = pulse_width.unwrap_or(*width);
74 if phase < w {
75 1.0
76 } else {
77 -1.0
78 }
79 }
80
81 LfoWaveform::SampleAndHold => {
82 phase
86 }
87
88 LfoWaveform::RandomWalk => {
89 phase
92 }
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct LfoState {
100 pub phase: f64,
102 pub value: f64,
104 pub hold_counter: usize,
106 pub rng_state: u64,
108 pub last_time: f64,
110}
111
112#[derive(Debug, Clone)]
114pub struct LfoAutomaton {
115 name: String,
117 frequency: f64,
119 amplitude: f64,
121 offset: f64,
123 waveform: LfoWaveform,
125 range: Range,
127 pulse_width: f64,
129 walk_rate: f64,
131}
132
133impl LfoAutomaton {
134 pub fn new(
136 name: &str,
137 frequency: f64,
138 amplitude: f64,
139 offset: f64,
140 waveform: LfoWaveform,
141 ) -> Self {
142 Self {
143 name: name.to_string(),
144 frequency: frequency.max(0.001),
145 amplitude,
146 offset,
147 waveform,
148 range: Range::bipolar(),
149 pulse_width: 0.5,
150 walk_rate: 0.1,
151 }
152 }
153
154 pub fn with_range(mut self, range: Range) -> Self {
155 self.range = range;
156 self
157 }
158
159 pub fn with_pulse_width(mut self, width: f64) -> Self {
160 self.pulse_width = width.clamp(0.01, 0.99);
161 self
162 }
163
164 pub fn with_walk_rate(mut self, rate: f64) -> Self {
165 self.walk_rate = rate.max(0.0);
166 self
167 }
168
169 fn random(&self, state: &mut u64) -> f64 {
170 let mut x = *state;
171 x ^= x << 13;
172 x ^= x >> 7;
173 x ^= x << 17;
174 *state = x;
175 (x as f64 / u64::MAX as f64) * 2.0 - 1.0
176 }
177
178 fn update_random_walk(&self, state: &mut LfoState, dt: f64) {
179 let step = (self.random(&mut state.rng_state) - 0.5) * self.walk_rate * dt * 100.0;
180 state.value = (state.value + step).clamp(-1.0, 1.0);
181 }
182}
183
184#[derive(Debug, Clone, Default)]
186pub enum LfoAction {
187 #[default]
188 None,
189 Reset,
191}
192
193impl Automaton for LfoAutomaton {
194 type State = LfoState;
195 type Action = LfoAction;
196
197 fn step(
198 &self,
199 time: Time,
200 action: &Self::Action,
201 state: &Self::State,
202 ) -> (Self::State, Option<f64>) {
203 let mut new_state = state.clone();
204
205 if let LfoAction::Reset = action {
207 new_state.phase = 0.0;
208 new_state.last_time = time;
209 }
210
211 let dt = time - new_state.last_time;
212
213 new_state.phase += self.frequency * dt;
214 if new_state.phase >= 1.0 {
215 new_state.phase -= 1.0;
216 if let LfoWaveform::SampleAndHold = self.waveform {
217 new_state.value = self.random(&mut new_state.rng_state);
218 }
219 }
220 new_state.last_time = time;
221
222 if let LfoWaveform::RandomWalk = self.waveform {
223 self.update_random_walk(&mut new_state, dt);
224 }
225
226 let raw_value = match self.waveform {
227 LfoWaveform::SampleAndHold => new_state.value,
228 LfoWaveform::RandomWalk => new_state.value,
229 _ => self
230 .waveform
231 .evaluate(new_state.phase, Some(self.pulse_width)),
232 };
233
234 let value = raw_value * self.amplitude + self.offset;
235 let clamped = self.range.clamp(value);
236
237 (new_state, Some(clamped))
238 }
239
240 fn initial_state(&self) -> Self::State {
241 LfoState {
242 phase: 0.0,
243 value: 0.0,
244 hold_counter: 0,
245 rng_state: 123456789,
246 last_time: 0.0,
247 }
248 }
249
250 fn name(&self) -> &str {
251 &self.name
252 }
253
254 fn extract_value(&self, state: &Self::State) -> f64 {
255 let raw = match self.waveform {
256 LfoWaveform::SampleAndHold => state.value,
257 LfoWaveform::RandomWalk => state.value,
258 _ => self.waveform.evaluate(state.phase, Some(self.pulse_width)),
259 };
260 self.range.clamp(raw * self.amplitude + self.offset)
261 }
262}
263
264#[cfg(test)]
269mod tests {
270 use super::*;
271 use float_cmp::approx_eq;
272
273 #[test]
274 fn test_sine_lfo() {
275 let lfo = LfoAutomaton::new("Sine", 1.0, 1.0, 0.0, LfoWaveform::Sine);
276 let state = lfo.initial_state();
277
278 let (new_state, value) = lfo.step(0.0, &LfoAction::None, &state);
279 assert!(approx_eq!(f64, value.unwrap(), 0.0, epsilon = 0.01));
280
281 let (_, value) = lfo.step(0.25, &LfoAction::None, &new_state);
282 assert!(approx_eq!(f64, value.unwrap(), 1.0, epsilon = 0.01));
283 }
284
285 #[test]
286 fn test_reset_action() {
287 let lfo = LfoAutomaton::new("Test", 1.0, 1.0, 0.0, LfoWaveform::Sine);
288 let mut state = lfo.initial_state();
289 state.phase = 0.5;
290
291 let (new_state, _) = lfo.step(1.0, &LfoAction::Reset, &state);
292 assert!(approx_eq!(f64, new_state.phase, 0.0, epsilon = 0.01));
293 }
294}