devalang_wasm/engine/audio/
lfo.rs1use std::f32::consts::PI;
4
5#[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#[derive(Debug, Clone, PartialEq)]
28pub enum LfoRate {
29 Hz(f32), TempoSync(f32), }
32
33impl LfoRate {
34 pub fn from_value(s: &str) -> Self {
39 if s.contains('/') {
40 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 s.parse::<f32>()
53 .map(LfoRate::Hz)
54 .unwrap_or(LfoRate::Hz(1.0))
55 }
56
57 pub fn to_hz(&self, bpm: f32) -> f32 {
59 match self {
60 LfoRate::Hz(hz) => *hz,
61 LfoRate::TempoSync(beats) => {
62 let beat_hz = bpm / 60.0;
65 beat_hz / beats
66 }
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct LfoParams {
74 pub rate: LfoRate, pub depth: f32, pub waveform: LfoWaveform,
77 pub target: LfoTarget, pub phase: f32, }
80
81#[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
114pub 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 raw_value * params.depth
129}
130
131pub 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
145fn lfo_sine(phase: f32) -> f32 {
148 (2.0 * PI * phase).sin()
149}
150
151fn lfo_triangle(phase: f32) -> f32 {
152 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 2.0 * phase - 1.0
163}
164
165#[cfg(test)]
166#[path = "test_lfo.rs"]
167mod tests;