devalang_wasm/engine/audio/
lfo.rs

1/// Low-Frequency Oscillator (LFO) module
2/// Provides modulation for various parameters (volume, pitch, filter cutoff, pan)
3use std::f32::consts::PI;
4
5/// LFO waveform types
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum LfoWaveform {
8    Sine,
9    Triangle,
10    Square,
11    Saw,
12}
13
14impl LfoWaveform {
15    pub fn from_str(s: &str) -> Self {
16        match s.to_lowercase().as_str() {
17            "sine" | "sin" => LfoWaveform::Sine,
18            "triangle" | "tri" => LfoWaveform::Triangle,
19            "square" | "sq" => LfoWaveform::Square,
20            "saw" | "sawtooth" => LfoWaveform::Saw,
21            _ => LfoWaveform::Sine,
22        }
23    }
24}
25
26/// LFO rate specification - Hz or tempo-synced
27#[derive(Debug, Clone, PartialEq)]
28pub enum LfoRate {
29    Hz(f32),        // Rate in Hz
30    TempoSync(f32), // Rate as fraction of beat (e.g., 1.0 = 1 beat, 0.25 = 1/4 beat)
31}
32
33impl LfoRate {
34    /// Parse rate from string or number
35    /// "4.0" or 4.0 => 4.0 Hz
36    /// "1/4" => 1/4 beat (tempo-synced)
37    /// "1/8" => 1/8 beat
38    pub fn from_value(s: &str) -> Self {
39        if s.contains('/') {
40            // Parse fraction like "1/4"
41            let parts: Vec<&str> = s.split('/').collect();
42            if parts.len() == 2 {
43                if let (Ok(num), Ok(denom)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>()) {
44                    if denom != 0.0 {
45                        return LfoRate::TempoSync(num / denom);
46                    }
47                }
48            }
49        }
50
51        // Try to parse as Hz
52        s.parse::<f32>()
53            .map(LfoRate::Hz)
54            .unwrap_or(LfoRate::Hz(1.0))
55    }
56
57    /// Convert to Hz given current BPM
58    pub fn to_hz(&self, bpm: f32) -> f32 {
59        match self {
60            LfoRate::Hz(hz) => *hz,
61            LfoRate::TempoSync(beats) => {
62                // Convert beat fraction to Hz
63                // e.g., 1/4 beat at 120 BPM = 120/60 * 4 = 8 Hz
64                let beat_hz = bpm / 60.0;
65                beat_hz / beats
66            }
67        }
68    }
69}
70
71/// LFO parameters
72#[derive(Debug, Clone)]
73pub struct LfoParams {
74    pub rate: LfoRate, // Frequency (Hz or tempo-synced)
75    pub depth: f32,    // Modulation depth (0.0-1.0)
76    pub waveform: LfoWaveform,
77    pub target: LfoTarget, // What parameter to modulate
78    pub phase: f32,        // Initial phase (0.0-1.0)
79}
80
81/// LFO modulation target
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum LfoTarget {
84    Volume,
85    Pitch,
86    FilterCutoff,
87    Pan,
88}
89
90impl LfoTarget {
91    pub fn from_str(s: &str) -> Option<Self> {
92        match s.to_lowercase().as_str() {
93            "volume" | "vol" | "amp" | "amplitude" => Some(LfoTarget::Volume),
94            "pitch" | "frequency" | "freq" => Some(LfoTarget::Pitch),
95            "filter" | "cutoff" | "filter_cutoff" => Some(LfoTarget::FilterCutoff),
96            "pan" | "panning" => Some(LfoTarget::Pan),
97            _ => None,
98        }
99    }
100}
101
102impl Default for LfoParams {
103    fn default() -> Self {
104        Self {
105            rate: LfoRate::Hz(1.0),
106            depth: 0.5,
107            waveform: LfoWaveform::Sine,
108            target: LfoTarget::Volume,
109            phase: 0.0,
110        }
111    }
112}
113
114/// Generate LFO value at a specific time
115/// Returns a value in the range [-1.0, 1.0]
116pub fn generate_lfo_value(params: &LfoParams, time_seconds: f32, bpm: f32) -> f32 {
117    let rate_hz = params.rate.to_hz(bpm);
118    let phase = (time_seconds * rate_hz + params.phase).fract();
119
120    let raw_value = match params.waveform {
121        LfoWaveform::Sine => lfo_sine(phase),
122        LfoWaveform::Triangle => lfo_triangle(phase),
123        LfoWaveform::Square => lfo_square(phase),
124        LfoWaveform::Saw => lfo_saw(phase),
125    };
126
127    // Scale by depth
128    raw_value * params.depth
129}
130
131/// Apply LFO modulation to a base value
132/// center_value: the base value to modulate around
133/// range: the maximum deviation from center
134pub fn apply_lfo_modulation(
135    params: &LfoParams,
136    time_seconds: f32,
137    bpm: f32,
138    center_value: f32,
139    range: f32,
140) -> f32 {
141    let lfo_value = generate_lfo_value(params, time_seconds, bpm);
142    center_value + (lfo_value * range)
143}
144
145// Waveform generators (phase is 0.0-1.0)
146
147fn lfo_sine(phase: f32) -> f32 {
148    (2.0 * PI * phase).sin()
149}
150
151fn lfo_triangle(phase: f32) -> f32 {
152    // Triangle wave: rises from -1 to 1 and back
153    4.0 * (phase - 0.5).abs() - 1.0
154}
155
156fn lfo_square(phase: f32) -> f32 {
157    if phase < 0.5 { 1.0 } else { -1.0 }
158}
159
160fn lfo_saw(phase: f32) -> f32 {
161    // Sawtooth: rises linearly from -1 to 1
162    2.0 * phase - 1.0
163}
164
165#[cfg(test)]
166#[path = "test_lfo.rs"]
167mod tests;