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 => {
56                if self.phase < 0.5 {
57                    1.0
58                } else {
59                    -1.0
60                }
61            }
62            3 => {
63                // Sample-and-hold: new random value on each cycle
64                if crossed_zero {
65                    self.held_value = pseudo_random(self.phase) * 2.0 - 1.0;
66                }
67                self.held_value
68            }
69            _ => {
70                // Random smooth: interpolate toward new target each cycle
71                if crossed_zero {
72                    self.smooth_target = pseudo_random(self.phase) * 2.0 - 1.0;
73                }
74                let smooth_rate = rate / sr * 0.1;
75                self.smooth_current += (self.smooth_target - self.smooth_current) * smooth_rate;
76                self.smooth_current
77            }
78        };
79
80        self.prev_phase = self.phase;
81        self.phase = (self.phase + phase_inc).fract();
82        raw * depth
83    }
84}
85
86/// Simple deterministic pseudo-random from phase value.
87#[inline(always)]
88fn pseudo_random(seed: f32) -> f32 {
89    let x = (seed * 127.1 + 311.7).sin() * 43_758.547;
90    x - x.floor()
91}
92
93impl Default for Lfo {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl DspNode for Lfo {
100    fn process(
101        &mut self,
102        _inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
103        output: &mut [f32; BUFFER_SIZE],
104        params: &mut ParamBlock,
105        sample_rate: f32,
106    ) {
107        for sample in output.iter_mut() {
108            let rate = params.get(0).current.clamp(0.01, 20.0);
109            let depth = params.get(1).current.clamp(0.0, 1.0);
110            let waveform = params.get(2).current as u32;
111            *sample = self.next_sample(rate, depth, waveform, sample_rate);
112            params.tick_all();
113        }
114    }
115
116    fn capture_state(&self) -> StateBlob {
117        StateBlob::from_value(&LfoState {
118            phase: self.phase,
119            held_value: self.held_value,
120            smooth_target: self.smooth_target,
121            smooth_current: self.smooth_current,
122        })
123    }
124
125    fn restore_state(&mut self, state: StateBlob) {
126        let s: LfoState = state.to_value();
127        self.phase = s.phase;
128        self.held_value = s.held_value;
129        self.smooth_target = s.smooth_target;
130        self.smooth_current = s.smooth_current;
131    }
132
133    fn type_name(&self) -> &'static str {
134        "Lfo"
135    }
136}