Skip to main content

aether_nodes/
lfo.rs

1//! Low-Frequency Oscillator (LFO) — modulation source.
2//!
3//! Outputs a -1.0 to 1.0 modulation signal. Not audio-rate.
4//!
5//! Param layout:
6//!   0 = rate     (Hz, 0.01 – 20.0)
7//!   1 = depth    (0.0 – 1.0)
8//!   2 = waveform (0=sine, 1=triangle, 2=square, 3=sample-and-hold, 4=random-smooth)
9//!   3 = phase    (0.0 – 1.0, initial phase offset)
10
11use aether_core::{node::DspNode, param::ParamBlock, state::StateBlob, BUFFER_SIZE, MAX_INPUTS};
12use std::f32::consts::TAU;
13
14#[derive(Clone, Copy)]
15struct LfoState {
16    phase: f32,
17    held_value: f32,
18    smooth_target: f32,
19    smooth_current: f32,
20}
21
22pub struct Lfo {
23    phase: f32,
24    held_value: f32,
25    smooth_target: f32,
26    smooth_current: f32,
27    prev_phase: f32,
28}
29
30impl Lfo {
31    pub fn new() -> Self {
32        Self {
33            phase: 0.0,
34            held_value: 0.0,
35            smooth_target: 0.0,
36            smooth_current: 0.0,
37            prev_phase: 0.0,
38        }
39    }
40
41    #[inline(always)]
42    fn next_sample(&mut self, rate: f32, depth: f32, waveform: u32, sr: f32) -> f32 {
43        let phase_inc = rate / sr;
44        let crossed_zero = self.phase < self.prev_phase; // wrapped around
45
46        let raw = match waveform {
47            0 => (self.phase * TAU).sin(),
48            1 => {
49                if self.phase < 0.5 {
50                    4.0 * self.phase - 1.0
51                } else {
52                    3.0 - 4.0 * self.phase
53                }
54            }
55            2 => if self.phase < 0.5 { 1.0 } else { -1.0 },
56            3 => {
57                // Sample-and-hold: new random value on each cycle
58                if crossed_zero {
59                    self.held_value = pseudo_random(self.phase) * 2.0 - 1.0;
60                }
61                self.held_value
62            }
63            _ => {
64                // Random smooth: interpolate toward new target each cycle
65                if crossed_zero {
66                    self.smooth_target = pseudo_random(self.phase) * 2.0 - 1.0;
67                }
68                let smooth_rate = rate / sr * 0.1;
69                self.smooth_current += (self.smooth_target - self.smooth_current) * smooth_rate;
70                self.smooth_current
71            }
72        };
73
74        self.prev_phase = self.phase;
75        self.phase = (self.phase + phase_inc).fract();
76        raw * depth
77    }
78}
79
80/// Simple deterministic pseudo-random from phase value.
81#[inline(always)]
82fn pseudo_random(seed: f32) -> f32 {
83    let x = (seed * 127.1 + 311.7).sin() * 43_758.547;
84    x - x.floor()
85}
86
87impl Default for Lfo {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl DspNode for Lfo {
94    fn process(
95        &mut self,
96        _inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
97        output: &mut [f32; BUFFER_SIZE],
98        params: &mut ParamBlock,
99        sample_rate: f32,
100    ) {
101        for sample in output.iter_mut() {
102            let rate = params.get(0).current.clamp(0.01, 20.0);
103            let depth = params.get(1).current.clamp(0.0, 1.0);
104            let waveform = params.get(2).current as u32;
105            *sample = self.next_sample(rate, depth, waveform, sample_rate);
106            params.tick_all();
107        }
108    }
109
110    fn capture_state(&self) -> StateBlob {
111        StateBlob::from_value(&LfoState {
112            phase: self.phase,
113            held_value: self.held_value,
114            smooth_target: self.smooth_target,
115            smooth_current: self.smooth_current,
116        })
117    }
118
119    fn restore_state(&mut self, state: StateBlob) {
120        let s: LfoState = state.to_value();
121        self.phase = s.phase;
122        self.held_value = s.held_value;
123        self.smooth_target = s.smooth_target;
124        self.smooth_current = s.smooth_current;
125    }
126
127    fn type_name(&self) -> &'static str {
128        "Lfo"
129    }
130}