use std::f32::consts::TAU;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LfoWaveform {
Sine,
Triangle,
Sawtooth,
Square,
Random,
}
#[derive(Debug, Clone)]
pub struct Lfo {
phase: f32,
phase_inc: f32,
waveform: LfoWaveform,
sample_rate: f32,
random_state: u32,
random_value: f32,
}
impl Lfo {
#[must_use]
pub fn new(frequency: f32, sample_rate: f32, waveform: LfoWaveform) -> Self {
let phase_inc = frequency / sample_rate;
Self {
phase: 0.0,
phase_inc,
waveform,
sample_rate,
random_state: 0x1234_5678,
random_value: 0.0,
}
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> f32 {
let value = match self.waveform {
LfoWaveform::Sine => (self.phase * TAU).sin(),
LfoWaveform::Triangle => {
if self.phase < 0.5 {
4.0 * self.phase - 1.0
} else {
3.0 - 4.0 * self.phase
}
}
LfoWaveform::Sawtooth => 2.0 * self.phase - 1.0,
LfoWaveform::Square => {
if self.phase < 0.5 {
-1.0
} else {
1.0
}
}
LfoWaveform::Random => {
if self.phase < self.phase_inc {
self.random_state = self
.random_state
.wrapping_mul(1_103_515_245)
.wrapping_add(12_345);
#[allow(clippy::cast_precision_loss)]
let random_val = (self.random_state as f32) / (u32::MAX as f32);
self.random_value = 2.0 * random_val - 1.0;
}
self.random_value
}
};
self.phase += self.phase_inc;
if self.phase >= 1.0 {
self.phase -= 1.0;
}
value
}
pub fn next_unipolar(&mut self) -> f32 {
(self.next() + 1.0) * 0.5
}
pub fn set_frequency(&mut self, frequency: f32) {
self.phase_inc = frequency / self.sample_rate;
}
pub fn set_waveform(&mut self, waveform: LfoWaveform) {
self.waveform = waveform;
}
pub fn reset(&mut self) {
self.phase = 0.0;
}
pub fn set_phase(&mut self, phase: f32) {
self.phase = phase.rem_euclid(1.0);
}
#[must_use]
pub fn phase(&self) -> f32 {
self.phase
}
}
#[derive(Debug, Clone)]
pub struct StereoLfo {
pub left: Lfo,
pub right: Lfo,
}
impl StereoLfo {
#[must_use]
pub fn new(frequency: f32, sample_rate: f32, waveform: LfoWaveform, phase_offset: f32) -> Self {
let left = Lfo::new(frequency, sample_rate, waveform);
let mut right = Lfo::new(frequency, sample_rate, waveform);
right.set_phase(phase_offset);
Self { left, right }
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> (f32, f32) {
(self.left.next(), self.right.next())
}
pub fn next_unipolar(&mut self) -> (f32, f32) {
(self.left.next_unipolar(), self.right.next_unipolar())
}
pub fn set_frequency(&mut self, frequency: f32) {
self.left.set_frequency(frequency);
self.right.set_frequency(frequency);
}
pub fn set_waveform(&mut self, waveform: LfoWaveform) {
self.left.set_waveform(waveform);
self.right.set_waveform(waveform);
}
pub fn reset(&mut self) {
self.left.reset();
self.right.reset();
}
}
#[derive(Debug, Clone)]
pub struct ParameterSmoother {
current: f32,
target: f32,
coefficient: f32,
}
impl ParameterSmoother {
#[must_use]
pub fn new(time_constant_ms: f32, sample_rate: f32) -> Self {
let coefficient = (-1000.0 / (time_constant_ms * sample_rate)).exp();
Self {
current: 0.0,
target: 0.0,
coefficient,
}
}
pub fn set_target(&mut self, target: f32) {
self.target = target;
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> f32 {
self.current = self.target + self.coefficient * (self.current - self.target);
self.current
}
pub fn reset(&mut self, value: f32) {
self.current = value;
self.target = value;
}
#[must_use]
pub fn is_stable(&self) -> bool {
(self.current - self.target).abs() < 1e-3
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lfo_sine() {
let mut lfo = Lfo::new(1.0, 100.0, LfoWaveform::Sine);
let v0 = lfo.next();
assert!(v0.abs() < 0.1);
for _ in 0..24 {
lfo.next();
}
let v25 = lfo.next();
assert!((v25 - 1.0).abs() < 0.1);
}
#[test]
fn test_lfo_triangle() {
let mut lfo = Lfo::new(1.0, 100.0, LfoWaveform::Triangle);
let v0 = lfo.next();
assert!((v0 + 1.0).abs() < 0.1);
let v1 = lfo.next();
assert!(v1 > v0);
}
#[test]
fn test_lfo_square() {
let mut lfo = Lfo::new(1.0, 100.0, LfoWaveform::Square);
for _ in 0..100 {
let val = lfo.next();
assert!(
val == -1.0 || val == 1.0,
"Square wave should only be -1.0 or 1.0"
);
}
}
#[test]
fn test_lfo_unipolar() {
let mut lfo = Lfo::new(1.0, 100.0, LfoWaveform::Sine);
for _ in 0..100 {
let val = lfo.next_unipolar();
assert!(val >= 0.0 && val <= 1.0);
}
}
#[test]
fn test_stereo_lfo() {
let mut lfo = StereoLfo::new(1.0, 100.0, LfoWaveform::Sine, 0.5);
let (l, r) = lfo.next();
assert!((l + r).abs() < 0.1);
}
#[test]
fn test_parameter_smoother() {
let mut smoother = ParameterSmoother::new(10.0, 48000.0);
smoother.reset(0.0);
smoother.set_target(1.0);
let v1 = smoother.next();
let v2 = smoother.next();
let v3 = smoother.next();
assert!(v1 > 0.0 && v1 < 1.0);
assert!(v2 > v1);
assert!(v3 > v2);
for _ in 0..100000 {
smoother.next();
}
assert!(smoother.is_stable());
}
#[test]
fn test_lfo_reset() {
let mut lfo = Lfo::new(1.0, 100.0, LfoWaveform::Sine);
for _ in 0..25 {
lfo.next();
}
assert!(lfo.phase() > 0.0);
lfo.reset();
assert_eq!(lfo.phase(), 0.0);
}
}