use std::f32::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LfoWaveform {
Sine,
Triangle,
Square,
Saw,
}
impl LfoWaveform {
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"sine" | "sin" => LfoWaveform::Sine,
"triangle" | "tri" => LfoWaveform::Triangle,
"square" | "sq" => LfoWaveform::Square,
"saw" | "sawtooth" => LfoWaveform::Saw,
_ => LfoWaveform::Sine,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum LfoRate {
Hz(f32), TempoSync(f32), }
impl LfoRate {
pub fn from_value(s: &str) -> Self {
if s.contains('/') {
let parts: Vec<&str> = s.split('/').collect();
if parts.len() == 2 {
if let (Ok(num), Ok(denom)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>()) {
if denom != 0.0 {
return LfoRate::TempoSync(num / denom);
}
}
}
}
s.parse::<f32>()
.map(LfoRate::Hz)
.unwrap_or(LfoRate::Hz(1.0))
}
pub fn to_hz(&self, bpm: f32) -> f32 {
match self {
LfoRate::Hz(hz) => *hz,
LfoRate::TempoSync(beats) => {
let beat_hz = bpm / 60.0;
beat_hz / beats
}
}
}
}
#[derive(Debug, Clone)]
pub struct LfoParams {
pub rate: LfoRate, pub depth: f32, pub waveform: LfoWaveform,
pub target: LfoTarget, pub phase: f32, }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LfoTarget {
Volume,
Pitch,
FilterCutoff,
Pan,
}
impl LfoTarget {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"volume" | "vol" | "amp" | "amplitude" => Some(LfoTarget::Volume),
"pitch" | "frequency" | "freq" => Some(LfoTarget::Pitch),
"filter" | "cutoff" | "filter_cutoff" => Some(LfoTarget::FilterCutoff),
"pan" | "panning" => Some(LfoTarget::Pan),
_ => None,
}
}
}
impl Default for LfoParams {
fn default() -> Self {
Self {
rate: LfoRate::Hz(1.0),
depth: 0.5,
waveform: LfoWaveform::Sine,
target: LfoTarget::Volume,
phase: 0.0,
}
}
}
pub fn generate_lfo_value(params: &LfoParams, time_seconds: f32, bpm: f32) -> f32 {
let rate_hz = params.rate.to_hz(bpm);
let phase = (time_seconds * rate_hz + params.phase).fract();
let raw_value = match params.waveform {
LfoWaveform::Sine => lfo_sine(phase),
LfoWaveform::Triangle => lfo_triangle(phase),
LfoWaveform::Square => lfo_square(phase),
LfoWaveform::Saw => lfo_saw(phase),
};
raw_value * params.depth
}
pub fn apply_lfo_modulation(
params: &LfoParams,
time_seconds: f32,
bpm: f32,
center_value: f32,
range: f32,
) -> f32 {
let lfo_value = generate_lfo_value(params, time_seconds, bpm);
center_value + (lfo_value * range)
}
fn lfo_sine(phase: f32) -> f32 {
(2.0 * PI * phase).sin()
}
fn lfo_triangle(phase: f32) -> f32 {
4.0 * (phase - 0.5).abs() - 1.0
}
fn lfo_square(phase: f32) -> f32 {
if phase < 0.5 { 1.0 } else { -1.0 }
}
fn lfo_saw(phase: f32) -> f32 {
2.0 * phase - 1.0
}
#[cfg(test)]
#[path = "test_lfo.rs"]
mod tests;